feat(api): add reset sort endpoints for server and node

This commit is contained in:
Chang lue Tsen 2025-09-01 11:41:34 -04:00
parent e910d0e345
commit 367ef9d2e7
12 changed files with 288 additions and 89 deletions

View File

@ -11,13 +11,17 @@ info (
import "../types.api" import "../types.api"
type ( type (
ServerOnlineIP {
IP string `json:"ip"`
Protocol string `json:"protocol"`
}
ServerOnlineUser { ServerOnlineUser {
IP []string `json:"ip"` IP []ServerOnlineIP `json:"ip"`
UserId int64 `json:"user_id"` UserId int64 `json:"user_id"`
Subscribe string `json:"subscribe"` Subscribe string `json:"subscribe"`
SubscribeId int64 `json:"subscribe_id"` SubscribeId int64 `json:"subscribe_id"`
Traffic int64 `json:"traffic"` Traffic int64 `json:"traffic"`
ExpiredAt int64 `json:"expired_at"` ExpiredAt int64 `json:"expired_at"`
} }
ServerStatus { ServerStatus {
Cpu float64 `json:"cpu"` Cpu float64 `json:"cpu"`
@ -27,18 +31,18 @@ type (
Online []ServerOnlineUser `json:"online"` Online []ServerOnlineUser `json:"online"`
} }
Server { Server {
Id int64 `json:"id"` Id int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Country string `json:"country"` Country string `json:"country"`
City string `json:"city"` City string `json:"city"`
Ratio float32 `json:"ratio"` Ratio float32 `json:"ratio"`
Address string `json:"address"` Address string `json:"address"`
Sort int `json:"sort"` Sort int `json:"sort"`
Protocols []Protocol `json:"protocols"` Protocols []Protocol `json:"protocols"`
LastReportedAt int64 `json:"last_reported_at"` LastReportedAt int64 `json:"last_reported_at"`
Status []ServerStatus `json:"status"` Status ServerStatus `json:"status"`
CreatedAt int64 `json:"created_at"` CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"` UpdatedAt int64 `json:"updated_at"`
} }
Protocol { Protocol {
Type string `json:"type"` Type string `json:"type"`
@ -159,6 +163,11 @@ type (
Fail uint64 `json:"fail"` Fail uint64 `json:"fail"`
Message string `json:"message,omitempty"` Message string `json:"message,omitempty"`
} }
ResetSortRequest {
Page int `json:"page"`
Size int `json:"size"`
Sort []int64 `json:"sort"`
}
) )
@server ( @server (
@ -214,5 +223,13 @@ service ppanel {
@doc "Migrate server and node data to new database" @doc "Migrate server and node data to new database"
@handler MigrateServerNode @handler MigrateServerNode
post /migrate/run returns (MigrateServerNodeResponse) post /migrate/run returns (MigrateServerNodeResponse)
@doc "Reset server sort"
@handler ResetSortWithServer
post /server/sort (ResetSortRequest)
@doc "Reset node sort"
@handler ResetSortWithNode
post /node/sort (ResetSortRequest)
} }

View File

@ -0,0 +1,26 @@
package server
import (
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/admin/server"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/result"
)
// Reset node sort
func ResetSortWithNodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
var req types.ResetSortRequest
_ = c.ShouldBind(&req)
validateErr := svcCtx.Validate(&req)
if validateErr != nil {
result.ParamErrorResult(c, validateErr)
return
}
l := server.NewResetSortWithNodeLogic(c.Request.Context(), svcCtx)
err := l.ResetSortWithNode(&req)
result.HttpResult(c, nil, err)
}
}

View File

@ -0,0 +1,26 @@
package server
import (
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/admin/server"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/result"
)
// Reset server sort
func ResetSortWithServerHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
var req types.ResetSortRequest
_ = c.ShouldBind(&req)
validateErr := svcCtx.Validate(&req)
if validateErr != nil {
result.ParamErrorResult(c, validateErr)
return
}
l := server.NewResetSortWithServerLogic(c.Request.Context(), svcCtx)
err := l.ResetSortWithServer(&req)
result.HttpResult(c, nil, err)
}
}

View File

@ -317,6 +317,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
// Filter Node List // Filter Node List
adminServerGroupRouter.GET("/node/list", adminServer.FilterNodeListHandler(serverCtx)) adminServerGroupRouter.GET("/node/list", adminServer.FilterNodeListHandler(serverCtx))
// Reset node sort
adminServerGroupRouter.POST("/node/sort", adminServer.ResetSortWithNodeHandler(serverCtx))
// Toggle Node Status // Toggle Node Status
adminServerGroupRouter.POST("/node/status/toggle", adminServer.ToggleNodeStatusHandler(serverCtx)) adminServerGroupRouter.POST("/node/status/toggle", adminServer.ToggleNodeStatusHandler(serverCtx))
@ -326,6 +329,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
// Get Server Protocols // Get Server Protocols
adminServerGroupRouter.GET("/protocols", adminServer.GetServerProtocolsHandler(serverCtx)) adminServerGroupRouter.GET("/protocols", adminServer.GetServerProtocolsHandler(serverCtx))
// Reset server sort
adminServerGroupRouter.POST("/server/sort", adminServer.ResetSortWithServerHandler(serverCtx))
// Update Server // Update Server
adminServerGroupRouter.POST("/update", adminServer.UpdateServerHandler(serverCtx)) adminServerGroupRouter.POST("/update", adminServer.UpdateServerHandler(serverCtx))
} }

View File

@ -11,6 +11,7 @@ import (
"github.com/perfect-panel/server/pkg/xerr" "github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
"gorm.io/gorm"
) )
type FilterServerListLogic struct { type FilterServerListLogic struct {
@ -54,8 +55,20 @@ func (l *FilterServerListLogic) FilterServerList(req *types.FilterServerListRequ
} }
tool.DeepCopy(&protocols, dst) tool.DeepCopy(&protocols, dst)
server.Protocols = protocols server.Protocols = protocols
// handler status
server.Status = l.handlerServerStatus(datum.Id, protocols) nodeStatus, err := l.svcCtx.NodeModel.StatusCache(l.ctx, datum.Id)
if err != nil {
if !errors.Is(err, redis.Nil) {
l.Errorw("[handlerServerStatus] GetNodeStatus Error: ", logger.Field("error", err.Error()), logger.Field("node_id", datum.Id))
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetNodeStatus Error")
}
server.Status = types.ServerStatus{
Mem: nodeStatus.Mem,
Cpu: nodeStatus.Cpu,
Disk: nodeStatus.Disk,
Online: l.handlerServerStatus(datum.Id, protocols),
}
list = append(list, server) list = append(list, server)
} }
@ -65,57 +78,71 @@ func (l *FilterServerListLogic) FilterServerList(req *types.FilterServerListRequ
}, nil }, nil
} }
func (l *FilterServerListLogic) handlerServerStatus(id int64, protocols []types.Protocol) []types.ServerStatus { func (l *FilterServerListLogic) handlerServerStatus(id int64, protocols []types.Protocol) []types.ServerOnlineUser {
var result []types.ServerStatus result := make([]types.ServerOnlineUser, 0)
for _, protocol := range protocols { for _, protocol := range protocols {
nodeStatus, err := l.svcCtx.NodeModel.StatusCache(l.ctx, id, protocol.Type) // query online user
data, err := l.svcCtx.NodeModel.OnlineUserSubscribe(l.ctx, id, protocol.Type)
if err != nil { if err != nil {
if !errors.Is(err, redis.Nil) { if !errors.Is(err, redis.Nil) {
l.Errorw("[handlerServerStatus] GetNodeStatus Error: ", logger.Field("error", err.Error()), logger.Field("node_id", id)) l.Errorw("[handlerServerStatus] OnlineUserSubscribe Error: ", logger.Field("error", err.Error()), logger.Field("node_id", id), logger.Field("protocol", protocol.Type))
} }
return result continue
} }
status := types.ServerStatus{ if len(data) > 0 {
Mem: nodeStatus.Mem, for sub, online := range data {
Cpu: nodeStatus.Cpu, var ips []types.ServerOnlineIP
Disk: nodeStatus.Disk, for _, ip := range online {
Protocol: protocol.Type, ips = append(ips, types.ServerOnlineIP{
Online: make([]types.ServerOnlineUser, 0), IP: ip,
Protocol: protocol.Type,
})
}
result = append(result, types.ServerOnlineUser{
IP: ips,
SubscribeId: sub,
})
}
} }
// parse online users }
onlineUser, err := l.svcCtx.NodeModel.OnlineUserSubscribe(l.ctx, id, protocol.Type) // merge same subscribe
if err != nil { var mapResult = make(map[int64]types.ServerOnlineUser)
l.Errorw("[handlerServerStatus] GetNodeOnlineUser Error: ", logger.Field("error", err.Error()), logger.Field("node_id", id)) for _, item := range result {
return result if exist, ok := mapResult[item.SubscribeId]; ok {
} // merge
var onlineList []types.ServerOnlineUser exist.Traffic += item.Traffic
var onlineMap = make(map[int64]types.ServerOnlineUser) exist.IP = append(exist.IP, item.IP...)
// group by user_id mapResult[item.SubscribeId] = exist
for subId, info := range onlineUser { } else {
data, err := l.svcCtx.UserModel.FindOneUserSubscribe(l.ctx, subId) // get subscribe info
info, err := l.svcCtx.UserModel.FindOneUserSubscribe(l.ctx, item.SubscribeId)
if err != nil { if err != nil {
l.Errorw("[handlerServerStatus] FindOneSubscribe Error: ", logger.Field("error", err.Error())) if !errors.Is(err, gorm.ErrRecordNotFound) {
l.Errorw("[handlerServerStatus] FindOneSubscribe Error: ", logger.Field("error", err.Error()), logger.Field("subscribe_id", item.SubscribeId))
}
continue continue
} }
if online, exist := onlineMap[data.UserId]; !exist { data := types.ServerOnlineUser{
onlineMap[data.UserId] = types.ServerOnlineUser{ IP: item.IP,
IP: info, UserId: info.UserId,
UserId: data.UserId, Subscribe: "",
Subscribe: data.Subscribe.Name, SubscribeId: item.SubscribeId,
SubscribeId: data.SubscribeId, Traffic: info.Download + info.Upload,
Traffic: data.Traffic, ExpiredAt: info.ExpireTime.UnixMilli(),
ExpiredAt: data.ExpireTime.UnixMilli(),
}
} else {
online.IP = append(online.IP, info...)
onlineMap[data.UserId] = online
} }
if info.Subscribe != nil {
data.Subscribe = info.Subscribe.Name
}
// add new
mapResult[item.SubscribeId] = data
} }
for _, online := range onlineMap { }
onlineList = append(onlineList, online) // convert map to slice
} result = make([]types.ServerOnlineUser, 0, len(mapResult))
status.Online = onlineList for _, item := range mapResult {
result = append(result, status) result = append(result, item)
} }
return result return result
} }

View File

@ -0,0 +1,30 @@
package server
import (
"context"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
)
type ResetSortWithNodeLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// Reset node sort
func NewResetSortWithNodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ResetSortWithNodeLogic {
return &ResetSortWithNodeLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *ResetSortWithNodeLogic) ResetSortWithNode(req *types.ResetSortRequest) error {
// todo: add your logic here and delete this line
return nil
}

View File

@ -0,0 +1,28 @@
package server
import (
"context"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
)
type ResetSortWithServerLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// NewResetSortWithServerLogic Reset server sort
func NewResetSortWithServerLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ResetSortWithServerLogic {
return &ResetSortWithServerLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *ResetSortWithServerLogic) ResetSortWithServer(req *types.ResetSortRequest) error {
return nil
}

View File

@ -16,7 +16,7 @@ type ServerPushStatusLogic struct {
svcCtx *svc.ServiceContext svcCtx *svc.ServiceContext
} }
// Push server status // NewServerPushStatusLogic Push server status
func NewServerPushStatusLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ServerPushStatusLogic { func NewServerPushStatusLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ServerPushStatusLogic {
return &ServerPushStatusLogic{ return &ServerPushStatusLogic{
Logger: logger.WithContext(ctx), Logger: logger.WithContext(ctx),
@ -32,7 +32,7 @@ func (l *ServerPushStatusLogic) ServerPushStatus(req *types.ServerPushStatusRequ
l.Errorw("[PushOnlineUsers] FindOne error", logger.Field("error", err)) l.Errorw("[PushOnlineUsers] FindOne error", logger.Field("error", err))
return errors.New("server not found") return errors.New("server not found")
} }
err = l.svcCtx.NodeModel.UpdateStatusCache(l.ctx, req.ServerId, req.Protocol, &node.Status{ err = l.svcCtx.NodeModel.UpdateStatusCache(l.ctx, req.ServerId, &node.Status{
Cpu: req.Cpu, Cpu: req.Cpu,
Mem: req.Mem, Mem: req.Mem,
Disk: req.Disk, Disk: req.Disk,

View File

@ -12,8 +12,8 @@ import (
type ( type (
customCacheLogicModel interface { customCacheLogicModel interface {
StatusCache(ctx context.Context, serverId int64, protocol string) (Status, error) StatusCache(ctx context.Context, serverId int64) (Status, error)
UpdateStatusCache(ctx context.Context, serverId int64, protocol string, status *Status) error UpdateStatusCache(ctx context.Context, serverId int64, status *Status) error
OnlineUserSubscribe(ctx context.Context, serverId int64, protocol string) (OnlineUserSubscribe, error) OnlineUserSubscribe(ctx context.Context, serverId int64, protocol string) (OnlineUserSubscribe, error)
UpdateOnlineUserSubscribe(ctx context.Context, serverId int64, protocol string, subscribe OnlineUserSubscribe) error UpdateOnlineUserSubscribe(ctx context.Context, serverId int64, protocol string, subscribe OnlineUserSubscribe) error
OnlineUserSubscribeGlobal(ctx context.Context) (int64, error) OnlineUserSubscribeGlobal(ctx context.Context) (int64, error)
@ -54,22 +54,22 @@ func (s *Status) Unmarshal(data string) error {
const ( const (
Expiry = 300 * time.Second // Cache expiry time in seconds Expiry = 300 * time.Second // Cache expiry time in seconds
StatusCacheKey = "node:status:%d:%s" // Node status cache key format (Server ID and protocol) Example: node:status:1:shadowsocks StatusCacheKey = "node:status:%d" // Node status cache key format (Server ID and protocol) Example: node:status:1:shadowsocks
OnlineUserCacheKeyWithSubscribe = "node:online:subscribe:%d:%s" // Online user subscribe cache key format (Server ID and protocol) Example: node:online:subscribe:1:shadowsocks OnlineUserCacheKeyWithSubscribe = "node:online:subscribe:%d:%s" // Online user subscribe cache key format (Server ID and protocol) Example: node:online:subscribe:1:shadowsocks
OnlineUserSubscribeCacheKeyWithGlobal = "node:online:subscribe:global" // Online user global subscribe cache key OnlineUserSubscribeCacheKeyWithGlobal = "node:online:subscribe:global" // Online user global subscribe cache key
) )
// UpdateStatusCache Update server status to cache // UpdateStatusCache Update server status to cache
func (m *customServerModel) UpdateStatusCache(ctx context.Context, serverId int64, protocol string, status *Status) error { func (m *customServerModel) UpdateStatusCache(ctx context.Context, serverId int64, status *Status) error {
key := fmt.Sprintf(StatusCacheKey, serverId, protocol) key := fmt.Sprintf(StatusCacheKey, serverId)
return m.Cache.Set(ctx, key, status.Marshal(), Expiry).Err() return m.Cache.Set(ctx, key, status.Marshal(), Expiry).Err()
} }
// StatusCache Get server status from cache // StatusCache Get server status from cache
func (m *customServerModel) StatusCache(ctx context.Context, serverId int64, protocol string) (Status, error) { func (m *customServerModel) StatusCache(ctx context.Context, serverId int64) (Status, error) {
var status Status var status Status
key := fmt.Sprintf(StatusCacheKey, serverId, protocol) key := fmt.Sprintf(StatusCacheKey, serverId)
result, err := m.Cache.Get(ctx, key).Result() result, err := m.Cache.Get(ctx, key).Result()
if err != nil { if err != nil {

View File

@ -1,6 +1,10 @@
package node package node
import "time" import (
"time"
"gorm.io/gorm"
)
type Node struct { type Node struct {
Id int64 `gorm:"primary_key"` Id int64 `gorm:"primary_key"`
@ -12,10 +16,22 @@ type Node struct {
Server *Server `gorm:"foreignKey:ServerId;references:Id"` Server *Server `gorm:"foreignKey:ServerId;references:Id"`
Protocol string `gorm:"type:varchar(100);not null;default:'';comment:Protocol"` Protocol string `gorm:"type:varchar(100);not null;default:'';comment:Protocol"`
Enabled *bool `gorm:"type:boolean;not null;default:true;comment:Enabled"` Enabled *bool `gorm:"type:boolean;not null;default:true;comment:Enabled"`
Sort int `gorm:"uniqueIndex;not null;default:0;comment:Sort"`
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
UpdatedAt time.Time `gorm:"comment:Update Time"` UpdatedAt time.Time `gorm:"comment:Update Time"`
} }
func (Node) TableName() string { func (n *Node) TableName() string {
return "nodes" return "nodes"
} }
func (n *Node) BeforeCreate(tx *gorm.DB) error {
if n.Sort == 0 {
var maxSort int
if err := tx.Model(&Node{}).Select("COALESCE(MAX(sort), 0)").Scan(&maxSort).Error; err != nil {
return err
}
n.Sort = maxSort + 1
}
return nil
}

View File

@ -5,6 +5,7 @@ import (
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
"gorm.io/gorm"
) )
type Server struct { type Server struct {
@ -25,6 +26,17 @@ func (*Server) TableName() string {
return "servers" return "servers"
} }
func (m *Server) BeforeCreate(tx *gorm.DB) error {
if m.Sort == 0 {
var maxSort int
if err := tx.Model(&Server{}).Select("COALESCE(MAX(sort), 0)").Scan(&maxSort).Error; err != nil {
return err
}
m.Sort = maxSort + 1
}
return nil
}
// MarshalProtocols Marshal server protocols to json // MarshalProtocols Marshal server protocols to json
func (m *Server) MarshalProtocols(list []Protocol) error { func (m *Server) MarshalProtocols(list []Protocol) error {
var validate = make(map[string]bool) var validate = make(map[string]bool)

View File

@ -1606,6 +1606,12 @@ type ResetPasswordRequest struct {
CfToken string `json:"cf_token,optional"` CfToken string `json:"cf_token,optional"`
} }
type ResetSortRequest struct {
Page int `json:"page"`
Size int `json:"size"`
Sort []int64 `json:"sort"`
}
type ResetSubscribeLog struct { type ResetSubscribeLog struct {
Type uint16 `json:"type"` Type uint16 `json:"type"`
UserId int64 `json:"user_id"` UserId int64 `json:"user_id"`
@ -1669,18 +1675,18 @@ type SendSmsCodeRequest struct {
} }
type Server struct { type Server struct {
Id int64 `json:"id"` Id int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Country string `json:"country"` Country string `json:"country"`
City string `json:"city"` City string `json:"city"`
Ratio float32 `json:"ratio"` Ratio float32 `json:"ratio"`
Address string `json:"address"` Address string `json:"address"`
Sort int `json:"sort"` Sort int `json:"sort"`
Protocols []Protocol `json:"protocols"` Protocols []Protocol `json:"protocols"`
LastReportedAt int64 `json:"last_reported_at"` LastReportedAt int64 `json:"last_reported_at"`
Status []ServerStatus `json:"status"` Status ServerStatus `json:"status"`
CreatedAt int64 `json:"created_at"` CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"` UpdatedAt int64 `json:"updated_at"`
} }
type ServerBasic struct { type ServerBasic struct {
@ -1702,13 +1708,18 @@ type ServerGroup struct {
UpdatedAt int64 `json:"updated_at"` UpdatedAt int64 `json:"updated_at"`
} }
type ServerOnlineIP struct {
IP string `json:"ip"`
Protocol string `json:"protocol"`
}
type ServerOnlineUser struct { type ServerOnlineUser struct {
IP []string `json:"ip"` IP []ServerOnlineIP `json:"ip"`
UserId int64 `json:"user_id"` UserId int64 `json:"user_id"`
Subscribe string `json:"subscribe"` Subscribe string `json:"subscribe"`
SubscribeId int64 `json:"subscribe_id"` SubscribeId int64 `json:"subscribe_id"`
Traffic int64 `json:"traffic"` Traffic int64 `json:"traffic"`
ExpiredAt int64 `json:"expired_at"` ExpiredAt int64 `json:"expired_at"`
} }
type ServerPushStatusRequest struct { type ServerPushStatusRequest struct {