From 367ef9d2e7a584684056cb3cb8031a8ca02db742 Mon Sep 17 00:00:00 2001 From: Chang lue Tsen Date: Mon, 1 Sep 2025 11:41:34 -0400 Subject: [PATCH] feat(api): add reset sort endpoints for server and node --- apis/admin/server.api | 53 ++++++--- .../admin/server/resetSortWithNodeHandler.go | 26 ++++ .../server/resetSortWithServerHandler.go | 26 ++++ internal/handler/routes.go | 6 + .../admin/server/filterServerListLogic.go | 111 +++++++++++------- .../admin/server/resetSortWithNodeLogic.go | 30 +++++ .../admin/server/resetSortWithServerLogic.go | 28 +++++ .../logic/server/serverPushStatusLogic.go | 4 +- internal/model/node/cache.go | 14 +-- internal/model/node/node.go | 20 +++- internal/model/node/server.go | 12 ++ internal/types/types.go | 47 +++++--- 12 files changed, 288 insertions(+), 89 deletions(-) create mode 100644 internal/handler/admin/server/resetSortWithNodeHandler.go create mode 100644 internal/handler/admin/server/resetSortWithServerHandler.go create mode 100644 internal/logic/admin/server/resetSortWithNodeLogic.go create mode 100644 internal/logic/admin/server/resetSortWithServerLogic.go diff --git a/apis/admin/server.api b/apis/admin/server.api index f4ab1ac..7a5f43b 100644 --- a/apis/admin/server.api +++ b/apis/admin/server.api @@ -11,13 +11,17 @@ info ( import "../types.api" type ( + ServerOnlineIP { + IP string `json:"ip"` + Protocol string `json:"protocol"` + } ServerOnlineUser { - IP []string `json:"ip"` - UserId int64 `json:"user_id"` - Subscribe string `json:"subscribe"` - SubscribeId int64 `json:"subscribe_id"` - Traffic int64 `json:"traffic"` - ExpiredAt int64 `json:"expired_at"` + IP []ServerOnlineIP `json:"ip"` + UserId int64 `json:"user_id"` + Subscribe string `json:"subscribe"` + SubscribeId int64 `json:"subscribe_id"` + Traffic int64 `json:"traffic"` + ExpiredAt int64 `json:"expired_at"` } ServerStatus { Cpu float64 `json:"cpu"` @@ -27,18 +31,18 @@ type ( Online []ServerOnlineUser `json:"online"` } Server { - Id int64 `json:"id"` - Name string `json:"name"` - Country string `json:"country"` - City string `json:"city"` - Ratio float32 `json:"ratio"` - Address string `json:"address"` - Sort int `json:"sort"` - Protocols []Protocol `json:"protocols"` - LastReportedAt int64 `json:"last_reported_at"` - Status []ServerStatus `json:"status"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` + Id int64 `json:"id"` + Name string `json:"name"` + Country string `json:"country"` + City string `json:"city"` + Ratio float32 `json:"ratio"` + Address string `json:"address"` + Sort int `json:"sort"` + Protocols []Protocol `json:"protocols"` + LastReportedAt int64 `json:"last_reported_at"` + Status ServerStatus `json:"status"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` } Protocol { Type string `json:"type"` @@ -159,6 +163,11 @@ type ( Fail uint64 `json:"fail"` Message string `json:"message,omitempty"` } + ResetSortRequest { + Page int `json:"page"` + Size int `json:"size"` + Sort []int64 `json:"sort"` + } ) @server ( @@ -214,5 +223,13 @@ service ppanel { @doc "Migrate server and node data to new database" @handler MigrateServerNode 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) } diff --git a/internal/handler/admin/server/resetSortWithNodeHandler.go b/internal/handler/admin/server/resetSortWithNodeHandler.go new file mode 100644 index 0000000..4b8b14c --- /dev/null +++ b/internal/handler/admin/server/resetSortWithNodeHandler.go @@ -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) + } +} diff --git a/internal/handler/admin/server/resetSortWithServerHandler.go b/internal/handler/admin/server/resetSortWithServerHandler.go new file mode 100644 index 0000000..7adbecb --- /dev/null +++ b/internal/handler/admin/server/resetSortWithServerHandler.go @@ -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) + } +} diff --git a/internal/handler/routes.go b/internal/handler/routes.go index 8af162d..1c876ef 100644 --- a/internal/handler/routes.go +++ b/internal/handler/routes.go @@ -317,6 +317,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Filter Node List adminServerGroupRouter.GET("/node/list", adminServer.FilterNodeListHandler(serverCtx)) + // Reset node sort + adminServerGroupRouter.POST("/node/sort", adminServer.ResetSortWithNodeHandler(serverCtx)) + // Toggle Node Status adminServerGroupRouter.POST("/node/status/toggle", adminServer.ToggleNodeStatusHandler(serverCtx)) @@ -326,6 +329,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Get Server Protocols adminServerGroupRouter.GET("/protocols", adminServer.GetServerProtocolsHandler(serverCtx)) + // Reset server sort + adminServerGroupRouter.POST("/server/sort", adminServer.ResetSortWithServerHandler(serverCtx)) + // Update Server adminServerGroupRouter.POST("/update", adminServer.UpdateServerHandler(serverCtx)) } diff --git a/internal/logic/admin/server/filterServerListLogic.go b/internal/logic/admin/server/filterServerListLogic.go index 1bfedaf..2e4aa04 100644 --- a/internal/logic/admin/server/filterServerListLogic.go +++ b/internal/logic/admin/server/filterServerListLogic.go @@ -11,6 +11,7 @@ import ( "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "github.com/redis/go-redis/v9" + "gorm.io/gorm" ) type FilterServerListLogic struct { @@ -54,8 +55,20 @@ func (l *FilterServerListLogic) FilterServerList(req *types.FilterServerListRequ } tool.DeepCopy(&protocols, dst) 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) } @@ -65,57 +78,71 @@ func (l *FilterServerListLogic) FilterServerList(req *types.FilterServerListRequ }, nil } -func (l *FilterServerListLogic) handlerServerStatus(id int64, protocols []types.Protocol) []types.ServerStatus { - var result []types.ServerStatus +func (l *FilterServerListLogic) handlerServerStatus(id int64, protocols []types.Protocol) []types.ServerOnlineUser { + result := make([]types.ServerOnlineUser, 0) + 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 !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{ - Mem: nodeStatus.Mem, - Cpu: nodeStatus.Cpu, - Disk: nodeStatus.Disk, - Protocol: protocol.Type, - Online: make([]types.ServerOnlineUser, 0), + if len(data) > 0 { + for sub, online := range data { + var ips []types.ServerOnlineIP + for _, ip := range online { + ips = append(ips, types.ServerOnlineIP{ + 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) - if err != nil { - l.Errorw("[handlerServerStatus] GetNodeOnlineUser Error: ", logger.Field("error", err.Error()), logger.Field("node_id", id)) - return result - } - var onlineList []types.ServerOnlineUser - var onlineMap = make(map[int64]types.ServerOnlineUser) - // group by user_id - for subId, info := range onlineUser { - data, err := l.svcCtx.UserModel.FindOneUserSubscribe(l.ctx, subId) + } + // merge same subscribe + var mapResult = make(map[int64]types.ServerOnlineUser) + for _, item := range result { + if exist, ok := mapResult[item.SubscribeId]; ok { + // merge + exist.Traffic += item.Traffic + exist.IP = append(exist.IP, item.IP...) + mapResult[item.SubscribeId] = exist + } else { + // get subscribe info + info, err := l.svcCtx.UserModel.FindOneUserSubscribe(l.ctx, item.SubscribeId) 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 } - if online, exist := onlineMap[data.UserId]; !exist { - onlineMap[data.UserId] = types.ServerOnlineUser{ - IP: info, - UserId: data.UserId, - Subscribe: data.Subscribe.Name, - SubscribeId: data.SubscribeId, - Traffic: data.Traffic, - ExpiredAt: data.ExpireTime.UnixMilli(), - } - } else { - online.IP = append(online.IP, info...) - onlineMap[data.UserId] = online + data := types.ServerOnlineUser{ + IP: item.IP, + UserId: info.UserId, + Subscribe: "", + SubscribeId: item.SubscribeId, + Traffic: info.Download + info.Upload, + ExpiredAt: info.ExpireTime.UnixMilli(), } + if info.Subscribe != nil { + data.Subscribe = info.Subscribe.Name + } + // add new + mapResult[item.SubscribeId] = data } - for _, online := range onlineMap { - onlineList = append(onlineList, online) - } - status.Online = onlineList - result = append(result, status) + } + // convert map to slice + result = make([]types.ServerOnlineUser, 0, len(mapResult)) + for _, item := range mapResult { + result = append(result, item) } return result } diff --git a/internal/logic/admin/server/resetSortWithNodeLogic.go b/internal/logic/admin/server/resetSortWithNodeLogic.go new file mode 100644 index 0000000..977db9f --- /dev/null +++ b/internal/logic/admin/server/resetSortWithNodeLogic.go @@ -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 +} diff --git a/internal/logic/admin/server/resetSortWithServerLogic.go b/internal/logic/admin/server/resetSortWithServerLogic.go new file mode 100644 index 0000000..16bffbe --- /dev/null +++ b/internal/logic/admin/server/resetSortWithServerLogic.go @@ -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 +} diff --git a/internal/logic/server/serverPushStatusLogic.go b/internal/logic/server/serverPushStatusLogic.go index 1cecb5a..c711011 100644 --- a/internal/logic/server/serverPushStatusLogic.go +++ b/internal/logic/server/serverPushStatusLogic.go @@ -16,7 +16,7 @@ type ServerPushStatusLogic struct { svcCtx *svc.ServiceContext } -// Push server status +// NewServerPushStatusLogic Push server status func NewServerPushStatusLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ServerPushStatusLogic { return &ServerPushStatusLogic{ Logger: logger.WithContext(ctx), @@ -32,7 +32,7 @@ func (l *ServerPushStatusLogic) ServerPushStatus(req *types.ServerPushStatusRequ l.Errorw("[PushOnlineUsers] FindOne error", logger.Field("error", err)) 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, Mem: req.Mem, Disk: req.Disk, diff --git a/internal/model/node/cache.go b/internal/model/node/cache.go index a217578..3e61c9f 100644 --- a/internal/model/node/cache.go +++ b/internal/model/node/cache.go @@ -12,8 +12,8 @@ import ( type ( customCacheLogicModel interface { - StatusCache(ctx context.Context, serverId int64, protocol string) (Status, error) - UpdateStatusCache(ctx context.Context, serverId int64, protocol string, status *Status) error + StatusCache(ctx context.Context, serverId int64) (Status, error) + UpdateStatusCache(ctx context.Context, serverId int64, status *Status) error OnlineUserSubscribe(ctx context.Context, serverId int64, protocol string) (OnlineUserSubscribe, error) UpdateOnlineUserSubscribe(ctx context.Context, serverId int64, protocol string, subscribe OnlineUserSubscribe) error OnlineUserSubscribeGlobal(ctx context.Context) (int64, error) @@ -54,22 +54,22 @@ func (s *Status) Unmarshal(data string) error { const ( 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 OnlineUserSubscribeCacheKeyWithGlobal = "node:online:subscribe:global" // Online user global subscribe cache key ) // UpdateStatusCache Update server status to cache -func (m *customServerModel) UpdateStatusCache(ctx context.Context, serverId int64, protocol string, status *Status) error { - key := fmt.Sprintf(StatusCacheKey, serverId, protocol) +func (m *customServerModel) UpdateStatusCache(ctx context.Context, serverId int64, status *Status) error { + key := fmt.Sprintf(StatusCacheKey, serverId) return m.Cache.Set(ctx, key, status.Marshal(), Expiry).Err() } // 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 - key := fmt.Sprintf(StatusCacheKey, serverId, protocol) + key := fmt.Sprintf(StatusCacheKey, serverId) result, err := m.Cache.Get(ctx, key).Result() if err != nil { diff --git a/internal/model/node/node.go b/internal/model/node/node.go index 7ec6de5..10d58a1 100644 --- a/internal/model/node/node.go +++ b/internal/model/node/node.go @@ -1,6 +1,10 @@ package node -import "time" +import ( + "time" + + "gorm.io/gorm" +) type Node struct { Id int64 `gorm:"primary_key"` @@ -12,10 +16,22 @@ type Node struct { Server *Server `gorm:"foreignKey:ServerId;references:Id"` Protocol string `gorm:"type:varchar(100);not null;default:'';comment:Protocol"` 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"` UpdatedAt time.Time `gorm:"comment:Update Time"` } -func (Node) TableName() string { +func (n *Node) TableName() string { 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 +} diff --git a/internal/model/node/server.go b/internal/model/node/server.go index e531a4a..dc75a70 100644 --- a/internal/model/node/server.go +++ b/internal/model/node/server.go @@ -5,6 +5,7 @@ import ( "time" "github.com/pkg/errors" + "gorm.io/gorm" ) type Server struct { @@ -25,6 +26,17 @@ func (*Server) TableName() string { 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 func (m *Server) MarshalProtocols(list []Protocol) error { var validate = make(map[string]bool) diff --git a/internal/types/types.go b/internal/types/types.go index f7f351f..d0cece2 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -1606,6 +1606,12 @@ type ResetPasswordRequest struct { 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 uint16 `json:"type"` UserId int64 `json:"user_id"` @@ -1669,18 +1675,18 @@ type SendSmsCodeRequest struct { } type Server struct { - Id int64 `json:"id"` - Name string `json:"name"` - Country string `json:"country"` - City string `json:"city"` - Ratio float32 `json:"ratio"` - Address string `json:"address"` - Sort int `json:"sort"` - Protocols []Protocol `json:"protocols"` - LastReportedAt int64 `json:"last_reported_at"` - Status []ServerStatus `json:"status"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` + Id int64 `json:"id"` + Name string `json:"name"` + Country string `json:"country"` + City string `json:"city"` + Ratio float32 `json:"ratio"` + Address string `json:"address"` + Sort int `json:"sort"` + Protocols []Protocol `json:"protocols"` + LastReportedAt int64 `json:"last_reported_at"` + Status ServerStatus `json:"status"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` } type ServerBasic struct { @@ -1702,13 +1708,18 @@ type ServerGroup struct { UpdatedAt int64 `json:"updated_at"` } +type ServerOnlineIP struct { + IP string `json:"ip"` + Protocol string `json:"protocol"` +} + type ServerOnlineUser struct { - IP []string `json:"ip"` - UserId int64 `json:"user_id"` - Subscribe string `json:"subscribe"` - SubscribeId int64 `json:"subscribe_id"` - Traffic int64 `json:"traffic"` - ExpiredAt int64 `json:"expired_at"` + IP []ServerOnlineIP `json:"ip"` + UserId int64 `json:"user_id"` + Subscribe string `json:"subscribe"` + SubscribeId int64 `json:"subscribe_id"` + Traffic int64 `json:"traffic"` + ExpiredAt int64 `json:"expired_at"` } type ServerPushStatusRequest struct {