diff --git a/adapter/adapter.go b/adapter/adapter.go index 7dda13f..a29ccae 100644 --- a/adapter/adapter.go +++ b/adapter/adapter.go @@ -1,26 +1,25 @@ package adapter import ( - "encoding/json" + "strings" - "github.com/perfect-panel/server/internal/model/server" + "github.com/perfect-panel/server/internal/model/node" "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/random" ) type Adapter struct { - SiteName string // 站点名称 - Servers []*server.Server // 服务器列表 - UserInfo User // 用户信息 - ClientTemplate string // 客户端配置模板 - OutputFormat string // 输出格式,默认是 base64 - SubscribeName string // 订阅名称 + SiteName string // 站点名称 + Servers []*node.Node // 服务器列表 + UserInfo User // 用户信息 + ClientTemplate string // 客户端配置模板 + OutputFormat string // 输出格式,默认是 base64 + SubscribeName string // 订阅名称 } type Option func(*Adapter) // WithServers 设置服务器列表 -func WithServers(servers []*server.Server) Option { +func WithServers(servers []*node.Node) Option { return func(opts *Adapter) { opts.Servers = servers } @@ -56,7 +55,7 @@ func WithSubscribeName(name string) Option { func NewAdapter(tpl string, opts ...Option) *Adapter { adapter := &Adapter{ - Servers: []*server.Server{}, + Servers: []*node.Node{}, UserInfo: User{}, ClientTemplate: tpl, OutputFormat: "base64", // 默认输出格式 @@ -87,51 +86,54 @@ func (adapter *Adapter) Client() (*Client, error) { return client, nil } -func (adapter *Adapter) Proxies(servers []*server.Server) ([]Proxy, error) { +func (adapter *Adapter) Proxies(servers []*node.Node) ([]Proxy, error) { var proxies []Proxy - for _, srv := range servers { - switch srv.RelayMode { - case server.RelayModeAll: - var relays []server.NodeRelay - if err := json.Unmarshal([]byte(srv.RelayNode), &relays); err != nil { - logger.Errorw("Unmarshal RelayNode", logger.Field("error", err.Error()), logger.Field("node", srv.Name), logger.Field("relayNode", srv.RelayNode)) - continue - } - for _, relay := range relays { - proxy, err := adapterProxy(*srv, relay.Host, uint64(relay.Port)) - if err != nil { - logger.Errorw("Adapter Proxy", logger.Field("error", err.Error()), logger.Field("node", srv.Name), logger.Field("relayNode", relay)) - continue - } - proxies = append(proxies, proxy) - } - case server.RelayModeRandom: - var relays []server.NodeRelay - if err := json.Unmarshal([]byte(srv.RelayNode), &relays); err != nil { - logger.Errorw("Unmarshal RelayNode", logger.Field("error", err.Error()), logger.Field("node", srv.Name), logger.Field("relayNode", srv.RelayNode)) - continue - } - randNum := random.RandomInRange(0, len(relays)-1) - relay := relays[randNum] - proxy, err := adapterProxy(*srv, relay.Host, uint64(relay.Port)) - if err != nil { - logger.Errorw("Adapter Proxy", logger.Field("error", err.Error()), logger.Field("node", srv.Name), logger.Field("relayNode", relay)) - continue - } - proxies = append(proxies, proxy) - - case server.RelayModeNone: - proxy, err := adapterProxy(*srv, srv.ServerAddr, 0) - if err != nil { - logger.Errorw("Adapter Proxy", logger.Field("error", err.Error()), logger.Field("node", srv.Name), logger.Field("serverAddr", srv.ServerAddr)) - continue - } - proxies = append(proxies, proxy) - default: - logger.Errorw("Unknown RelayMode", logger.Field("node", srv.Name), logger.Field("relayMode", srv.RelayMode)) + for _, item := range servers { + if item.Server == nil { + logger.Errorf("[Adapter] Server is nil for node ID: %d", item.Id) + continue + } + protocols, err := item.Server.UnmarshalProtocols() + if err != nil { + logger.Errorf("[Adapter] Unmarshal Protocols error: %s; server id : %d", err.Error(), item.ServerId) + continue + } + for _, protocol := range protocols { + if protocol.Type == item.Protocol { + proxies = append(proxies, Proxy{ + Name: item.Name, + Server: item.Address, + Port: item.Port, + Type: item.Protocol, + Tags: strings.Split(item.Tags, ","), + Security: protocol.Security, + SNI: protocol.SNI, + AllowInsecure: protocol.AllowInsecure, + Fingerprint: protocol.Fingerprint, + RealityServerAddr: protocol.RealityServerAddr, + RealityServerPort: protocol.RealityServerPort, + RealityPrivateKey: protocol.RealityPrivateKey, + RealityPublicKey: protocol.RealityPublicKey, + RealityShortId: protocol.RealityShortId, + Transport: protocol.Transport, + Host: protocol.Host, + Path: protocol.Path, + ServiceName: protocol.ServiceName, + Method: protocol.Cipher, + ServerKey: protocol.ServerKey, + Flow: protocol.Flow, + HopPorts: protocol.HopPorts, + HopInterval: protocol.HopInterval, + ObfsPassword: protocol.ObfsPassword, + DisableSNI: protocol.DisableSNI, + ReduceRtt: protocol.ReduceRtt, + UDPRelayMode: protocol.UDPRelayMode, + CongestionController: protocol.CongestionController, + }) + } } - } + return proxies, nil } diff --git a/adapter/client.go b/adapter/client.go index 89c11ae..0922e94 100644 --- a/adapter/client.go +++ b/adapter/client.go @@ -13,7 +13,7 @@ import ( type Proxy struct { Name string Server string - Port uint64 + Port uint16 Type string Tags []string diff --git a/adapter/utils.go b/adapter/utils.go index dd87cf3..b8e8da3 100644 --- a/adapter/utils.go +++ b/adapter/utils.go @@ -1,113 +1 @@ package adapter - -import ( - "encoding/json" - "fmt" - "strings" - - "github.com/perfect-panel/server/internal/model/server" - "github.com/perfect-panel/server/pkg/tool" -) - -func adapterProxy(svr server.Server, host string, port uint64) (Proxy, error) { - tags := strings.Split(svr.Tags, ",") - if len(tags) > 0 { - tags = tool.RemoveDuplicateElements(tags...) - } - node := Proxy{ - Name: svr.Name, - Host: host, - Port: port, - Type: svr.Protocol, - Tags: tags, - } - switch svr.Protocol { - case "shadowsocks": - var ss server.Shadowsocks - if err := json.Unmarshal([]byte(svr.Config), &ss); err != nil { - return node, fmt.Errorf("unmarshal shadowsocks config: %v", err.Error()) - } - if port == 0 { - node.Port = uint64(ss.Port) - } - node.Method = ss.Method - node.ServerKey = ss.ServerKey - case "vless": - var vless server.Vless - if err := json.Unmarshal([]byte(svr.Config), &vless); err != nil { - return node, fmt.Errorf("unmarshal vless config: %v", err.Error()) - } - if port == 0 { - node.Port = uint64(vless.Port) - } - node.Flow = vless.Flow - node.Transport = vless.Transport - tool.DeepCopy(&node, vless.TransportConfig) - node.Security = vless.Security - tool.DeepCopy(&node, vless.SecurityConfig) - case "vmess": - var vmess server.Vmess - if err := json.Unmarshal([]byte(svr.Config), &vmess); err != nil { - return node, fmt.Errorf("unmarshal vmess config: %v", err.Error()) - } - if port == 0 { - node.Port = uint64(vmess.Port) - } - node.Flow = vmess.Flow - node.Transport = vmess.Transport - tool.DeepCopy(&node, vmess.TransportConfig) - node.Security = vmess.Security - tool.DeepCopy(&node, vmess.SecurityConfig) - case "trojan": - var trojan server.Trojan - if err := json.Unmarshal([]byte(svr.Config), &trojan); err != nil { - return node, fmt.Errorf("unmarshal trojan config: %v", err.Error()) - } - if port == 0 { - node.Port = uint64(trojan.Port) - - } - - node.Flow = trojan.Flow - node.Transport = trojan.Transport - tool.DeepCopy(&node, trojan.TransportConfig) - node.Security = trojan.Security - tool.DeepCopy(&node, trojan.SecurityConfig) - case "hysteria2": - var hysteria2 server.Hysteria2 - if err := json.Unmarshal([]byte(svr.Config), &hysteria2); err != nil { - return node, fmt.Errorf("unmarshal hysteria2 config: %v", err.Error()) - } - if port == 0 { - node.Port = uint64(hysteria2.Port) - } - node.HopPorts = hysteria2.HopPorts - node.HopInterval = hysteria2.HopInterval - node.ObfsPassword = hysteria2.ObfsPassword - tool.DeepCopy(&node, hysteria2.SecurityConfig) - case "tuic": - var tuic server.Tuic - if err := json.Unmarshal([]byte(svr.Config), &tuic); err != nil { - return node, fmt.Errorf("unmarshal tuic config: %v", err.Error()) - } - if port == 0 { - node.Port = uint64(tuic.Port) - } - node.DisableSNI = tuic.DisableSNI - node.ReduceRtt = tuic.ReduceRtt - node.UDPRelayMode = tuic.UDPRelayMode - node.CongestionController = tuic.CongestionController - case "anytls": - var anytls server.AnyTLS - if err := json.Unmarshal([]byte(svr.Config), &anytls); err != nil { - return node, fmt.Errorf("unmarshal anytls config: %v", err.Error()) - } - if port == 0 { - node.Port = uint64(anytls.Port) - } - tool.DeepCopy(&node, anytls.SecurityConfig) - default: - return node, fmt.Errorf("unsupported protocol: %s", svr.Protocol) - } - return node, nil -} diff --git a/apis/admin/server.api b/apis/admin/server.api index a2a35eb..39ddc46 100644 --- a/apis/admin/server.api +++ b/apis/admin/server.api @@ -150,6 +150,14 @@ type ( Total int64 `json:"total"` List []Node `json:"list"` } + HasMigrateSeverNodeResponse { + HasMigrate bool `json:"has_migrate"` + } + MigrateServerNodeResponse { + Succee uint64 `json:"succee"` + Fail uint64 `json:"fail"` + Message string `json:"message,omitempty"` + } ) @server ( @@ -197,5 +205,13 @@ service ppanel { @doc "Toggle Node Status" @handler ToggleNodeStatus post /node/status/toggle (ToggleNodeStatusRequest) + + @doc "Check if there is any server or node to migrate" + @handler HasMigrateSeverNode + get /migrate/has returns (HasMigrateSeverNodeResponse) + + @doc "Migrate server and node data to new database" + @handler MigrateServerNode + post /migrate/run returns (MigrateServerNodeResponse) } diff --git a/apis/admin/subscribe.api b/apis/admin/subscribe.api index 4e79c03..f98179e 100644 --- a/apis/admin/subscribe.api +++ b/apis/admin/subscribe.api @@ -46,8 +46,8 @@ type ( DeviceLimit int64 `json:"device_limit"` Quota int64 `json:"quota"` GroupId int64 `json:"group_id"` - ServerGroup []int64 `json:"server_group"` - Server []int64 `json:"server"` + Nodes []int64 `json:"nodes"` + NodeTags []string `json:"node_tags"` Show *bool `json:"show"` Sell *bool `json:"sell"` DeductionRatio int64 `json:"deduction_ratio"` @@ -69,8 +69,8 @@ type ( DeviceLimit int64 `json:"device_limit"` Quota int64 `json:"quota"` GroupId int64 `json:"group_id"` - ServerGroup []int64 `json:"server_group"` - Server []int64 `json:"server"` + Nodes []int64 `json:"nodes"` + NodeTags []string `json:"node_tags"` Show *bool `json:"show"` Sell *bool `json:"sell"` Sort int64 `json:"sort"` diff --git a/apis/types.api b/apis/types.api index f4a1930..827142e 100644 --- a/apis/types.api +++ b/apis/types.api @@ -194,8 +194,8 @@ type ( DeviceLimit int64 `json:"device_limit"` Quota int64 `json:"quota"` GroupId int64 `json:"group_id"` - ServerGroup []int64 `json:"server_group"` - Server []int64 `json:"server"` + Nodes []int64 `json:"nodes"` + NodeTags []string `json:"node_tags"` Show bool `json:"show"` Sell bool `json:"sell"` Sort int64 `json:"sort"` diff --git a/initialize/migrate/database/02105_node.down.sql b/initialize/migrate/database/02105_node.down.sql index 4bd2529..210462e 100644 --- a/initialize/migrate/database/02105_node.down.sql +++ b/initialize/migrate/database/02105_node.down.sql @@ -1 +1,2 @@ -DROP TABLE IF EXISTS `servers`; \ No newline at end of file +DROP TABLE IF EXISTS `nodes`; +DROP TABLE IF EXISTS `servers`; diff --git a/initialize/migrate/database/02105_node.up.sql b/initialize/migrate/database/02105_node.up.sql index cb9a016..c9c310b 100644 --- a/initialize/migrate/database/02105_node.up.sql +++ b/initialize/migrate/database/02105_node.up.sql @@ -11,4 +11,18 @@ CREATE TABLE IF NOT EXISTS `servers` ( `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; \ No newline at end of file +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `nodes` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(100) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Node Name', + `tags` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Tags', + `port` smallint unsigned NOT NULL DEFAULT '0' COMMENT 'Connect Port', + `address` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Connect Address', + `server_id` bigint NOT NULL DEFAULT '0' COMMENT 'Server ID', + `protocol` varchar(100) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Protocol', + `enabled` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Enabled', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; diff --git a/initialize/migrate/database/02106_subscribe.down.sql b/initialize/migrate/database/02106_subscribe.down.sql new file mode 100644 index 0000000..20984b7 --- /dev/null +++ b/initialize/migrate/database/02106_subscribe.down.sql @@ -0,0 +1,5 @@ +ALTER TABLE `subscribe` +DROP COLUMN `nodes`, + DROP COLUMN `node_tags`, + ADD COLUMN `server` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'Server', + ADD COLUMN `server_group` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'Server Group'; diff --git a/initialize/migrate/database/02106_subscribe.up.sql b/initialize/migrate/database/02106_subscribe.up.sql new file mode 100644 index 0000000..28f5db5 --- /dev/null +++ b/initialize/migrate/database/02106_subscribe.up.sql @@ -0,0 +1,7 @@ +ALTER TABLE `subscribe` +ADD COLUMN `nodes` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'Node IDs', +ADD COLUMN `node_tags` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'Node Tags', +DROP COLUMN `server`, +DROP COLUMN `server_group`; + +DROP TABLE IF EXISTS `server_rule_group`; diff --git a/initialize/migrate/migrate_test.go b/initialize/migrate/migrate_test.go index c22e52b..531266e 100644 --- a/initialize/migrate/migrate_test.go +++ b/initialize/migrate/migrate_test.go @@ -3,7 +3,10 @@ package migrate import ( "testing" + "github.com/perfect-panel/server/internal/model/node" "github.com/perfect-panel/server/pkg/orm" + "gorm.io/driver/mysql" + "gorm.io/gorm" ) func getDSN() string { @@ -30,3 +33,17 @@ func TestMigrate(t *testing.T) { t.Log("migrate success") } } +func TestMysql(t *testing.T) { + db, err := gorm.Open(mysql.New(mysql.Config{ + DSN: "root:mylove520@tcp(localhost:3306)/vpnboard", + })) + if err != nil { + t.Fatalf("Failed to connect to MySQL: %v", err) + } + err = db.Migrator().AutoMigrate(&node.Node{}) + if err != nil { + t.Fatalf("Failed to auto migrate: %v", err) + return + } + t.Log("MySQL connection and migration successful") +} diff --git a/initialize/statistics.go b/initialize/statistics.go index 7ab02ad..144b31e 100644 --- a/initialize/statistics.go +++ b/initialize/statistics.go @@ -18,7 +18,7 @@ func TrafficDataToRedis(svcCtx *svc.ServiceContext) { } var nodeCacheData []cache.NodeTodayTrafficRank for _, node := range nodeData { - serverInfo, err := svcCtx.ServerModel.FindOne(ctx, node.ServerId) + serverInfo, err := svcCtx.NodeModel.FindOneServer(ctx, node.ServerId) if err != nil { logger.Errorw("查询节点信息失败", logger.Field("error", err.Error())) continue diff --git a/internal/handler/admin/server/hasMigrateSeverNodeHandler.go b/internal/handler/admin/server/hasMigrateSeverNodeHandler.go new file mode 100644 index 0000000..6088577 --- /dev/null +++ b/internal/handler/admin/server/hasMigrateSeverNodeHandler.go @@ -0,0 +1,18 @@ +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/pkg/result" +) + +// Check if there is any server or node to migrate +func HasMigrateSeverNodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := server.NewHasMigrateSeverNodeLogic(c.Request.Context(), svcCtx) + resp, err := l.HasMigrateSeverNode() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/server/migrateServerNodeHandler.go b/internal/handler/admin/server/migrateServerNodeHandler.go new file mode 100644 index 0000000..8f8c842 --- /dev/null +++ b/internal/handler/admin/server/migrateServerNodeHandler.go @@ -0,0 +1,18 @@ +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/pkg/result" +) + +// Migrate server and node data to new database +func MigrateServerNodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := server.NewMigrateServerNodeLogic(c.Request.Context(), svcCtx) + resp, err := l.MigrateServerNode() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/routes.go b/internal/handler/routes.go index 9ba4dac..dfff2ee 100644 --- a/internal/handler/routes.go +++ b/internal/handler/routes.go @@ -296,6 +296,12 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Filter Server List adminServerGroupRouter.GET("/list", adminServer.FilterServerListHandler(serverCtx)) + // Check if there is any server or node to migrate + adminServerGroupRouter.GET("/migrate/has", adminServer.HasMigrateSeverNodeHandler(serverCtx)) + + // Migrate server and node data to new database + adminServerGroupRouter.POST("/migrate/run", adminServer.MigrateServerNodeHandler(serverCtx)) + // Create Node adminServerGroupRouter.POST("/node/create", adminServer.CreateNodeHandler(serverCtx)) diff --git a/internal/logic/admin/application/previewSubscribeTemplateLogic.go b/internal/logic/admin/application/previewSubscribeTemplateLogic.go index 1781127..824aac5 100644 --- a/internal/logic/admin/application/previewSubscribeTemplateLogic.go +++ b/internal/logic/admin/application/previewSubscribeTemplateLogic.go @@ -5,6 +5,7 @@ import ( "time" "github.com/perfect-panel/server/adapter" + "github.com/perfect-panel/server/internal/model/node" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/logger" @@ -28,7 +29,10 @@ func NewPreviewSubscribeTemplateLogic(ctx context.Context, svcCtx *svc.ServiceCo } func (l *PreviewSubscribeTemplateLogic) PreviewSubscribeTemplate(req *types.PreviewSubscribeTemplateRequest) (resp *types.PreviewSubscribeTemplateResponse, err error) { - servers, err := l.svcCtx.ServerModel.FindAllServer(l.ctx) + _, servers, err := l.svcCtx.NodeModel.FilterNodeList(l.ctx, &node.FilterNodeParams{ + Page: 1, + Size: 1000, + }) if err != nil { l.Errorf("[PreviewSubscribeTemplateLogic] FindAllServer error: %v", err.Error()) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindAllServer error: %v", err.Error()) diff --git a/internal/logic/admin/server/constant.go b/internal/logic/admin/server/constant.go new file mode 100644 index 0000000..ae06fea --- /dev/null +++ b/internal/logic/admin/server/constant.go @@ -0,0 +1,11 @@ +package server + +const ( + ShadowSocks = "shadowsocks" + Vmess = "vmess" + Vless = "vless" + Trojan = "trojan" + AnyTLS = "anytls" + Tuic = "tuic" + Hysteria2 = "hysteria2" +) diff --git a/internal/logic/admin/server/filterNodeListLogic.go b/internal/logic/admin/server/filterNodeListLogic.go index 308e9d6..6404fd6 100644 --- a/internal/logic/admin/server/filterNodeListLogic.go +++ b/internal/logic/admin/server/filterNodeListLogic.go @@ -28,7 +28,7 @@ func NewFilterNodeListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Fi } func (l *FilterNodeListLogic) FilterNodeList(req *types.FilterNodeListRequest) (resp *types.FilterNodeListResponse, err error) { - total, data, err := l.svcCtx.NodeModel.FilterNodeList(l.ctx, &node.FilterParams{ + total, data, err := l.svcCtx.NodeModel.FilterNodeList(l.ctx, &node.FilterNodeParams{ Page: req.Page, Size: req.Size, Search: req.Search, diff --git a/internal/logic/admin/server/filterServerListLogic.go b/internal/logic/admin/server/filterServerListLogic.go index c6b11e4..5a74231 100644 --- a/internal/logic/admin/server/filterServerListLogic.go +++ b/internal/logic/admin/server/filterServerListLogic.go @@ -10,6 +10,7 @@ import ( "github.com/perfect-panel/server/pkg/tool" "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" + "github.com/redis/go-redis/v9" ) type FilterServerListLogic struct { @@ -68,7 +69,9 @@ func (l *FilterServerListLogic) handlerServerStatus(id int64) types.ServerStatus var result types.ServerStatus nodeStatus, err := l.svcCtx.NodeCache.GetNodeStatus(l.ctx, id) if err != nil { - l.Errorw("[handlerServerStatus] GetNodeStatus Error: ", logger.Field("error", err.Error()), logger.Field("node_id", id)) + if !errors.Is(err, redis.Nil) { + l.Errorw("[handlerServerStatus] GetNodeStatus Error: ", logger.Field("error", err.Error()), logger.Field("node_id", id)) + } return result } result = types.ServerStatus{ diff --git a/internal/logic/admin/server/hasMigrateSeverNodeLogic.go b/internal/logic/admin/server/hasMigrateSeverNodeLogic.go new file mode 100644 index 0000000..128b7f6 --- /dev/null +++ b/internal/logic/admin/server/hasMigrateSeverNodeLogic.go @@ -0,0 +1,52 @@ +package server + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/internal/model/server" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type HasMigrateSeverNodeLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewHasMigrateSeverNodeLogic Check if there is any server or node to migrate +func NewHasMigrateSeverNodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *HasMigrateSeverNodeLogic { + return &HasMigrateSeverNodeLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *HasMigrateSeverNodeLogic) HasMigrateSeverNode() (resp *types.HasMigrateSeverNodeResponse, err error) { + var oldCount, newCount int64 + query := l.svcCtx.DB.WithContext(l.ctx) + + err = query.Model(&server.Server{}).Count(&oldCount).Error + if err != nil { + l.Errorw("[HasMigrateSeverNode] Query Old Server Count Error: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "[HasMigrateSeverNode] Query Old Server Count Error") + } + err = query.Model(&node.Server{}).Count(&newCount).Error + if err != nil { + l.Errorw("[HasMigrateSeverNode] Query New Server Count Error: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "[HasMigrateSeverNode] Query New Server Count Error") + } + var shouldMigrate bool + if oldCount != 0 && newCount == 0 { + shouldMigrate = true + } + + return &types.HasMigrateSeverNodeResponse{ + HasMigrate: shouldMigrate, + }, nil +} diff --git a/internal/logic/admin/server/migrateServerNodeLogic.go b/internal/logic/admin/server/migrateServerNodeLogic.go new file mode 100644 index 0000000..e11032d --- /dev/null +++ b/internal/logic/admin/server/migrateServerNodeLogic.go @@ -0,0 +1,330 @@ +package server + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/internal/model/server" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" +) + +type MigrateServerNodeLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewMigrateServerNodeLogic Migrate server and node data to new database +func NewMigrateServerNodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *MigrateServerNodeLogic { + return &MigrateServerNodeLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *MigrateServerNodeLogic) MigrateServerNode() (resp *types.MigrateServerNodeResponse, err error) { + tx := l.svcCtx.DB.WithContext(l.ctx).Begin() + var oldServers []*server.Server + var newServers []*node.Server + var newNodes []*node.Node + + err = tx.Model(&server.Server{}).Find(&oldServers).Error + if err != nil { + l.Errorw("[MigrateServerNode] Query Old Server List Error: ", logger.Field("error", err.Error())) + return &types.MigrateServerNodeResponse{ + Succee: 0, + Fail: 0, + Message: fmt.Sprintf("Query Old Server List Error: %s", err.Error()), + }, nil + } + for _, oldServer := range oldServers { + data, err := l.adapterServer(oldServer) + if err != nil { + l.Errorw("[MigrateServerNode] Adapter Server Error: ", logger.Field("error", err.Error())) + if resp == nil { + resp = &types.MigrateServerNodeResponse{} + } + resp.Fail++ + if resp.Message == "" { + resp.Message = fmt.Sprintf("Adapter Server Error: %s", err.Error()) + } else { + resp.Message = fmt.Sprintf("%s; Adapter Server Error: %s", resp.Message, err.Error()) + } + continue + } + newServers = append(newServers, data) + + newNode, err := l.adapterNode(oldServer) + if err != nil { + l.Errorw("[MigrateServerNode] Adapter Node Error: ", logger.Field("error", err.Error())) + if resp == nil { + resp = &types.MigrateServerNodeResponse{} + } + resp.Fail++ + if resp.Message == "" { + resp.Message = fmt.Sprintf("Adapter Node Error: %s", err.Error()) + } else { + resp.Message = fmt.Sprintf("%s; Adapter Node Error: %s", resp.Message, err.Error()) + } + continue + } + for _, item := range newNode { + if item.Port == 0 { + protocols, _ := data.UnmarshalProtocols() + if len(protocols) > 0 { + item.Port = protocols[0].Port + } + } + newNodes = append(newNodes, item) + } + } + + if len(newServers) > 0 { + err = tx.Model(&node.Server{}).CreateInBatches(newServers, 20).Error + if err != nil { + tx.Rollback() + l.Errorw("[MigrateServerNode] Insert New Server List Error: ", logger.Field("error", err.Error())) + return &types.MigrateServerNodeResponse{ + Succee: 0, + Fail: uint64(len(newServers)), + Message: fmt.Sprintf("Insert New Server List Error: %s", err.Error()), + }, nil + } + } + if len(newNodes) > 0 { + err = tx.Model(&node.Node{}).CreateInBatches(newNodes, 20).Error + if err != nil { + tx.Rollback() + l.Errorw("[MigrateServerNode] Insert New Node List Error: ", logger.Field("error", err.Error())) + return &types.MigrateServerNodeResponse{ + Succee: uint64(len(newServers)), + Fail: uint64(len(newNodes)), + Message: fmt.Sprintf("Insert New Node List Error: %s", err.Error()), + }, nil + } + } + tx.Commit() + + return &types.MigrateServerNodeResponse{ + Succee: uint64(len(newServers)), + Fail: 0, + Message: fmt.Sprintf("Migrate Success: %d servers and %d nodes", len(newServers), len(newNodes)), + }, nil +} + +func (l *MigrateServerNodeLogic) adapterServer(info *server.Server) (*node.Server, error) { + result := &node.Server{ + Name: info.Name, + Country: info.Country, + City: info.City, + Ratio: info.TrafficRatio, + Address: info.ServerAddr, + Sort: int(info.Sort), + Protocols: "", + } + var protocols []node.Protocol + + switch info.Protocol { + case ShadowSocks: + var src server.Shadowsocks + err := json.Unmarshal([]byte(info.Config), &src) + if err != nil { + return nil, err + } + protocols = append(protocols, node.Protocol{ + Type: "shadowsocks", + Cipher: src.Method, + Port: uint16(src.Port), + ServerKey: src.ServerKey, + }) + case Vmess: + var src server.Vmess + err := json.Unmarshal([]byte(info.Config), &src) + if err != nil { + return nil, err + } + protocol := node.Protocol{ + Type: "vmess", + Port: uint16(src.Port), + Security: src.Security, + SNI: src.SecurityConfig.SNI, + AllowInsecure: src.SecurityConfig.AllowInsecure, + Fingerprint: src.SecurityConfig.Fingerprint, + RealityServerAddr: src.SecurityConfig.RealityServerAddr, + RealityServerPort: src.SecurityConfig.RealityServerPort, + RealityPrivateKey: src.SecurityConfig.RealityPrivateKey, + RealityPublicKey: src.SecurityConfig.RealityPublicKey, + RealityShortId: src.SecurityConfig.RealityShortId, + Transport: src.Transport, + Host: src.TransportConfig.Host, + Path: src.TransportConfig.Path, + ServiceName: src.TransportConfig.ServiceName, + Flow: src.Flow, + } + protocols = append(protocols, protocol) + protocols = append(protocols, protocol) + case Vless: + var src server.Vless + err := json.Unmarshal([]byte(info.Config), &src) + if err != nil { + return nil, err + } + protocol := node.Protocol{ + Type: "vless", + Port: uint16(src.Port), + Security: src.Security, + SNI: src.SecurityConfig.SNI, + AllowInsecure: src.SecurityConfig.AllowInsecure, + Fingerprint: src.SecurityConfig.Fingerprint, + RealityServerAddr: src.SecurityConfig.RealityServerAddr, + RealityServerPort: src.SecurityConfig.RealityServerPort, + RealityPrivateKey: src.SecurityConfig.RealityPrivateKey, + RealityPublicKey: src.SecurityConfig.RealityPublicKey, + RealityShortId: src.SecurityConfig.RealityShortId, + Transport: src.Transport, + Host: src.TransportConfig.Host, + Path: src.TransportConfig.Path, + ServiceName: src.TransportConfig.ServiceName, + Flow: src.Flow, + } + protocols = append(protocols, protocol) + case Trojan: + var src server.Trojan + err := json.Unmarshal([]byte(info.Config), &src) + if err != nil { + return nil, err + } + protocol := node.Protocol{ + Type: "trojan", + Port: uint16(src.Port), + Security: src.Security, + SNI: src.SecurityConfig.SNI, + AllowInsecure: src.SecurityConfig.AllowInsecure, + Fingerprint: src.SecurityConfig.Fingerprint, + RealityServerAddr: src.SecurityConfig.RealityServerAddr, + RealityServerPort: src.SecurityConfig.RealityServerPort, + RealityPrivateKey: src.SecurityConfig.RealityPrivateKey, + RealityPublicKey: src.SecurityConfig.RealityPublicKey, + RealityShortId: src.SecurityConfig.RealityShortId, + Transport: src.Transport, + Host: src.TransportConfig.Host, + Path: src.TransportConfig.Path, + ServiceName: src.TransportConfig.ServiceName, + Flow: src.Flow, + } + protocols = append(protocols, protocol) + case Hysteria2: + var src server.Hysteria2 + err := json.Unmarshal([]byte(info.Config), &src) + if err != nil { + return nil, err + } + protocol := node.Protocol{ + Type: "hysteria2", + Port: uint16(src.Port), + HopPorts: src.HopPorts, + HopInterval: src.HopInterval, + ObfsPassword: src.ObfsPassword, + SNI: src.SecurityConfig.SNI, + AllowInsecure: src.SecurityConfig.AllowInsecure, + Fingerprint: src.SecurityConfig.Fingerprint, + RealityServerAddr: src.SecurityConfig.RealityServerAddr, + RealityServerPort: src.SecurityConfig.RealityServerPort, + RealityPrivateKey: src.SecurityConfig.RealityPrivateKey, + RealityPublicKey: src.SecurityConfig.RealityPublicKey, + RealityShortId: src.SecurityConfig.RealityShortId, + } + protocols = append(protocols, protocol) + case Tuic: + var src server.Tuic + err := json.Unmarshal([]byte(info.Config), &src) + if err != nil { + return nil, err + } + protocol := node.Protocol{ + Type: "tuic", + Port: uint16(src.Port), + DisableSNI: src.DisableSNI, + ReduceRtt: src.ReduceRtt, + UDPRelayMode: src.UDPRelayMode, + CongestionController: src.CongestionController, + SNI: src.SecurityConfig.SNI, + AllowInsecure: src.SecurityConfig.AllowInsecure, + Fingerprint: src.SecurityConfig.Fingerprint, + RealityServerAddr: src.SecurityConfig.RealityServerAddr, + RealityServerPort: src.SecurityConfig.RealityServerPort, + RealityPrivateKey: src.SecurityConfig.RealityPrivateKey, + RealityPublicKey: src.SecurityConfig.RealityPublicKey, + RealityShortId: src.SecurityConfig.RealityShortId, + } + protocols = append(protocols, protocol) + case AnyTLS: + var src server.AnyTLS + err := json.Unmarshal([]byte(info.Config), &src) + if err != nil { + return nil, err + } + protocol := node.Protocol{ + Type: "anytls", + Port: uint16(src.Port), + SNI: src.SecurityConfig.SNI, + AllowInsecure: src.SecurityConfig.AllowInsecure, + Fingerprint: src.SecurityConfig.Fingerprint, + RealityServerAddr: src.SecurityConfig.RealityServerAddr, + RealityServerPort: src.SecurityConfig.RealityServerPort, + RealityPrivateKey: src.SecurityConfig.RealityPrivateKey, + RealityPublicKey: src.SecurityConfig.RealityPublicKey, + RealityShortId: src.SecurityConfig.RealityShortId, + } + protocols = append(protocols, protocol) + } + if len(protocols) > 0 { + err := result.MarshalProtocols(protocols) + if err != nil { + return nil, err + } + } + + return result, nil +} + +func (l *MigrateServerNodeLogic) adapterNode(info *server.Server) ([]*node.Node, error) { + var nodes []*node.Node + enable := true + switch info.RelayMode { + case server.RelayModeNone: + nodes = append(nodes, &node.Node{ + Name: info.Name, + Tags: "", + Port: 0, + Address: info.ServerAddr, + ServerId: info.Id, + Protocol: info.Protocol, + Enabled: &enable, + }) + default: + var relays []server.NodeRelay + err := json.Unmarshal([]byte(info.RelayNode), &relays) + if err != nil { + return nil, err + } + for _, relay := range relays { + nodes = append(nodes, &node.Node{ + Name: relay.Prefix + info.Name, + Tags: "", + Port: uint16(relay.Port), + Address: relay.Host, + ServerId: info.Id, + Protocol: info.Protocol, + Enabled: &enable, + }) + } + } + + return nodes, nil +} diff --git a/internal/logic/admin/subscribe/createSubscribeLogic.go b/internal/logic/admin/subscribe/createSubscribeLogic.go index 729bf5b..333b908 100644 --- a/internal/logic/admin/subscribe/createSubscribeLogic.go +++ b/internal/logic/admin/subscribe/createSubscribeLogic.go @@ -48,8 +48,8 @@ func (l *CreateSubscribeLogic) CreateSubscribe(req *types.CreateSubscribeRequest DeviceLimit: req.DeviceLimit, Quota: req.Quota, GroupId: req.GroupId, - ServerGroup: tool.Int64SliceToString(req.ServerGroup), - Server: tool.Int64SliceToString(req.Server), + Nodes: tool.Int64SliceToString(req.Nodes), + NodeTags: tool.StringSliceToString(req.NodeTags), Show: req.Show, Sell: req.Sell, Sort: 0, diff --git a/internal/logic/admin/subscribe/getSubscribeDetailsLogic.go b/internal/logic/admin/subscribe/getSubscribeDetailsLogic.go index 856cbf7..6defdf1 100644 --- a/internal/logic/admin/subscribe/getSubscribeDetailsLogic.go +++ b/internal/logic/admin/subscribe/getSubscribeDetailsLogic.go @@ -3,6 +3,7 @@ package subscribe import ( "context" "encoding/json" + "strings" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" @@ -41,7 +42,7 @@ func (l *GetSubscribeDetailsLogic) GetSubscribeDetails(req *types.GetSubscribeDe l.Logger.Error("[GetSubscribeDetailsLogic] JSON unmarshal failed: ", logger.Field("error", err.Error()), logger.Field("discount", sub.Discount)) } } - resp.Server = tool.StringToInt64Slice(sub.Server) - resp.ServerGroup = tool.StringToInt64Slice(sub.ServerGroup) + resp.Nodes = tool.StringToInt64Slice(sub.Nodes) + resp.NodeTags = strings.Split(sub.NodeTags, ",") return resp, nil } diff --git a/internal/logic/admin/subscribe/getSubscribeListLogic.go b/internal/logic/admin/subscribe/getSubscribeListLogic.go index 7168afb..c518336 100644 --- a/internal/logic/admin/subscribe/getSubscribeListLogic.go +++ b/internal/logic/admin/subscribe/getSubscribeListLogic.go @@ -3,6 +3,7 @@ package subscribe import ( "context" "encoding/json" + "strings" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" @@ -47,8 +48,8 @@ func (l *GetSubscribeListLogic) GetSubscribeList(req *types.GetSubscribeListRequ l.Logger.Error("[GetSubscribeListLogic] JSON unmarshal failed: ", logger.Field("error", err.Error()), logger.Field("discount", item.Discount)) } } - sub.Server = tool.StringToInt64Slice(item.Server) - sub.ServerGroup = tool.StringToInt64Slice(item.ServerGroup) + sub.Nodes = tool.StringToInt64Slice(item.Nodes) + sub.NodeTags = strings.Split(item.NodeTags, ",") resultList = append(resultList, sub) } diff --git a/internal/logic/admin/subscribe/updateSubscribeLogic.go b/internal/logic/admin/subscribe/updateSubscribeLogic.go index e04e4ef..dcc6836 100644 --- a/internal/logic/admin/subscribe/updateSubscribeLogic.go +++ b/internal/logic/admin/subscribe/updateSubscribeLogic.go @@ -56,8 +56,8 @@ func (l *UpdateSubscribeLogic) UpdateSubscribe(req *types.UpdateSubscribeRequest DeviceLimit: req.DeviceLimit, Quota: req.Quota, GroupId: req.GroupId, - ServerGroup: tool.Int64SliceToString(req.ServerGroup), - Server: tool.Int64SliceToString(req.Server), + Nodes: tool.Int64SliceToString(req.Nodes), + NodeTags: tool.StringSliceToString(req.NodeTags), Show: req.Show, Sell: req.Sell, Sort: req.Sort, diff --git a/internal/logic/auth/resetPasswordLogic.go b/internal/logic/auth/resetPasswordLogic.go index d13089f..3fa3d97 100644 --- a/internal/logic/auth/resetPasswordLogic.go +++ b/internal/logic/auth/resetPasswordLogic.go @@ -51,7 +51,7 @@ func (l *ResetPasswordLogic) ResetPassword(req *types.ResetPasswordRequest) (res Success: loginStatus, } content, _ := loginLog.Marshal() - if err = l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + if err := l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ Id: 0, Type: log.TypeLogin.Uint8(), Date: time.Now().Format("2006-01-02"), diff --git a/internal/logic/auth/telephoneLoginLogic.go b/internal/logic/auth/telephoneLoginLogic.go index 39b7a5e..ab80e60 100644 --- a/internal/logic/auth/telephoneLoginLogic.go +++ b/internal/logic/auth/telephoneLoginLogic.go @@ -59,7 +59,7 @@ func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r Success: loginStatus, } content, _ := loginLog.Marshal() - if err = l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + if err := l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ Id: 0, Type: log.TypeLogin.Uint8(), Date: time.Now().Format("2006-01-02"), diff --git a/internal/logic/auth/telephoneResetPasswordLogic.go b/internal/logic/auth/telephoneResetPasswordLogic.go index a2408d1..059196a 100644 --- a/internal/logic/auth/telephoneResetPasswordLogic.go +++ b/internal/logic/auth/telephoneResetPasswordLogic.go @@ -110,7 +110,7 @@ func (l *TelephoneResetPasswordLogic) TelephoneResetPassword(req *types.Telephon Success: token != "", } content, _ := loginLog.Marshal() - if err = l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + if err := l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ Id: 0, Type: log.TypeLogin.Uint8(), Date: time.Now().Format("2006-01-02"), diff --git a/internal/logic/auth/telephoneUserRegisterLogic.go b/internal/logic/auth/telephoneUserRegisterLogic.go index 341d6d3..8273f79 100644 --- a/internal/logic/auth/telephoneUserRegisterLogic.go +++ b/internal/logic/auth/telephoneUserRegisterLogic.go @@ -165,7 +165,7 @@ func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneR Success: token != "", } content, _ := loginLog.Marshal() - if err = l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + if err := l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ Id: 0, Type: log.TypeLogin.Uint8(), Date: time.Now().Format("2006-01-02"), @@ -188,7 +188,7 @@ func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneR RegisterTime: time.Now().UnixMilli(), } content, _ = registerLog.Marshal() - if err = l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + if err := l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ Type: log.TypeRegister.Uint8(), ObjectID: userInfo.Id, Date: time.Now().Format("2006-01-02"), diff --git a/internal/logic/auth/userLoginLogic.go b/internal/logic/auth/userLoginLogic.go index 9063188..8a30e95 100644 --- a/internal/logic/auth/userLoginLogic.go +++ b/internal/logic/auth/userLoginLogic.go @@ -49,7 +49,7 @@ func (l *UserLoginLogic) UserLogin(req *types.UserLoginRequest) (resp *types.Log Success: loginStatus, } content, _ := loginLog.Marshal() - if err = l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + if err := l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ Type: log.TypeLogin.Uint8(), Date: time.Now().Format("2006-01-02"), ObjectID: userInfo.Id, diff --git a/internal/logic/auth/userRegisterLogic.go b/internal/logic/auth/userRegisterLogic.go index 6871a3a..e2eca17 100644 --- a/internal/logic/auth/userRegisterLogic.go +++ b/internal/logic/auth/userRegisterLogic.go @@ -153,7 +153,7 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp * Success: loginStatus, } content, _ := loginLog.Marshal() - if err = l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + if err := l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ Id: 0, Type: log.TypeLogin.Uint8(), Date: time.Now().Format("2006-01-02"), diff --git a/internal/logic/server/constant.go b/internal/logic/server/constant.go index 0c54202..55ecc26 100644 --- a/internal/logic/server/constant.go +++ b/internal/logic/server/constant.go @@ -1,3 +1,83 @@ package server -const Unchanged = "Unchanged" +const ( + Unchanged = "Unchanged" + ShadowSocks = "shadowsocks" + Vmess = "vmess" + Vless = "vless" + Trojan = "trojan" + AnyTLS = "anytls" + Tuic = "tuic" + Hysteria2 = "hysteria2" +) + +type SecurityConfig struct { + SNI string `json:"sni"` + AllowInsecure *bool `json:"allow_insecure"` + Fingerprint string `json:"fingerprint"` + RealityServerAddress string `json:"reality_server_addr"` + RealityServerPort int `json:"reality_server_port"` + RealityPrivateKey string `json:"reality_private_key"` + RealityPublicKey string `json:"reality_public_key"` + RealityShortId string `json:"reality_short_id"` + RealityMldsa65seed string `json:"reality_mldsa65seed"` +} + +type TransportConfig struct { + Path string `json:"path"` + Host string `json:"host"` + ServiceName string `json:"service_name"` + DisableSNI bool `json:"disable_sni"` + ReduceRtt bool `json:"reduce_rtt"` + UDPRelayMode string `json:"udp_relay_mode"` + CongestionController string `json:"congestion_controller"` +} + +type VlessNode struct { + Port uint16 `json:"port"` + Flow string `json:"flow"` + Network string `json:"transport"` + TransportConfig *TransportConfig `json:"transport_config"` + Security string `json:"security"` + SecurityConfig *SecurityConfig `json:"security_config"` +} + +type VmessNode struct { + Port uint16 `json:"port"` + Network string `json:"transport"` + TransportConfig *TransportConfig `json:"transport_config"` + Security string `json:"security"` + SecurityConfig *SecurityConfig `json:"security_config"` +} + +type ShadowsocksNode struct { + Port uint16 `json:"port"` + Cipher string `json:"method"` + ServerKey string `json:"server_key"` +} + +type TrojanNode struct { + Port uint16 `json:"port"` + Network string `json:"transport"` + TransportConfig *TransportConfig `json:"transport_config"` + Security string `json:"security"` + SecurityConfig *SecurityConfig `json:"security_config"` +} + +type AnyTLSNode struct { + Port uint16 `json:"port"` + SecurityConfig *SecurityConfig `json:"security_config"` +} + +type TuicNode struct { + Port uint16 `json:"port"` + SecurityConfig *SecurityConfig `json:"security_config"` +} + +type Hysteria2Node struct { + Port uint16 `json:"port"` + HopPorts string `json:"hop_ports"` + HopInterval int `json:"hop_interval"` + ObfsPassword string `json:"obfs_password"` + SecurityConfig *SecurityConfig `json:"security_config"` +} diff --git a/internal/logic/server/getServerConfigLogic.go b/internal/logic/server/getServerConfigLogic.go index 7c8eaa9..fa1d1ca 100644 --- a/internal/logic/server/getServerConfigLogic.go +++ b/internal/logic/server/getServerConfigLogic.go @@ -1,11 +1,11 @@ package server import ( - "encoding/base64" "encoding/json" "fmt" "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/model/node" "github.com/perfect-panel/server/internal/config" "github.com/perfect-panel/server/internal/svc" @@ -51,21 +51,21 @@ func (l *GetServerConfigLogic) GetServerConfig(req *types.GetServerConfigRequest return resp, nil } } - nodeInfo, err := l.svcCtx.ServerModel.FindOne(l.ctx, req.ServerId) + data, err := l.svcCtx.NodeModel.FindOneServer(l.ctx, req.ServerId) if err != nil { l.Errorw("[GetServerConfig] FindOne error", logger.Field("error", err.Error())) return nil, err } - cfg := make(map[string]interface{}) - err = json.Unmarshal([]byte(nodeInfo.Config), &cfg) + + protocols, err := data.UnmarshalProtocols() if err != nil { - l.Errorw("[GetServerConfig] json unmarshal error", logger.Field("error", err.Error())) return nil, err } - - if nodeInfo.Protocol == "shadowsocks" { - if value, ok := cfg["server_key"]; ok && value != "" { - cfg["server_key"] = base64.StdEncoding.EncodeToString([]byte(value.(string))) + var cfg map[string]interface{} + for _, protocol := range protocols { + if protocol.Type == req.Protocol { + cfg = l.compatible(protocol) + break } } @@ -74,18 +74,162 @@ func (l *GetServerConfigLogic) GetServerConfig(req *types.GetServerConfigRequest PullInterval: l.svcCtx.Config.Node.NodePullInterval, PushInterval: l.svcCtx.Config.Node.NodePushInterval, }, - Protocol: nodeInfo.Protocol, + Protocol: req.Protocol, Config: cfg, } - data, err := json.Marshal(resp) + c, err := json.Marshal(resp) if err != nil { l.Errorw("[GetServerConfig] json marshal error", logger.Field("error", err.Error())) return nil, err } - etag := tool.GenerateETag(data) + etag := tool.GenerateETag(c) l.ctx.Header("ETag", etag) - if err = l.svcCtx.Redis.Set(l.ctx, cacheKey, data, -1).Err(); err != nil { + if err = l.svcCtx.Redis.Set(l.ctx, cacheKey, c, -1).Err(); err != nil { l.Errorw("[GetServerConfig] redis set error", logger.Field("error", err.Error())) } + // Check If-None-Match header + match := l.ctx.GetHeader("If-None-Match") + if match == etag { + return nil, xerr.StatusNotModified + } + return resp, nil } + +func (l *GetServerConfigLogic) compatible(config node.Protocol) map[string]interface{} { + var result interface{} + switch config.Type { + case ShadowSocks: + result = ShadowsocksNode{ + Port: config.Port, + Cipher: config.Cipher, + ServerKey: config.ServerKey, + } + case Vless: + result = VlessNode{ + Port: config.Port, + Flow: config.Flow, + Network: config.Transport, + TransportConfig: &TransportConfig{ + Path: config.Path, + Host: config.Host, + ServiceName: config.ServiceName, + DisableSNI: config.DisableSNI, + ReduceRtt: config.ReduceRtt, + UDPRelayMode: config.UDPRelayMode, + CongestionController: config.CongestionController, + }, + Security: config.Security, + SecurityConfig: &SecurityConfig{ + SNI: config.SNI, + AllowInsecure: &config.AllowInsecure, + Fingerprint: config.Fingerprint, + RealityServerAddress: config.RealityServerAddr, + RealityServerPort: config.RealityServerPort, + RealityPrivateKey: config.RealityPrivateKey, + RealityPublicKey: config.RealityPublicKey, + RealityShortId: config.RealityShortId, + }, + } + case Vmess: + result = VmessNode{ + Port: config.Port, + Network: config.Transport, + TransportConfig: &TransportConfig{ + Path: config.Path, + Host: config.Host, + ServiceName: config.ServiceName, + DisableSNI: config.DisableSNI, + ReduceRtt: config.ReduceRtt, + UDPRelayMode: config.UDPRelayMode, + CongestionController: config.CongestionController, + }, + Security: config.Security, + SecurityConfig: &SecurityConfig{ + SNI: config.SNI, + AllowInsecure: &config.AllowInsecure, + Fingerprint: config.Fingerprint, + RealityServerAddress: config.RealityServerAddr, + RealityServerPort: config.RealityServerPort, + RealityPrivateKey: config.RealityPrivateKey, + RealityPublicKey: config.RealityPublicKey, + RealityShortId: config.RealityShortId, + }, + } + case Trojan: + result = TrojanNode{ + Port: config.Port, + Network: config.Transport, + TransportConfig: &TransportConfig{ + Path: config.Path, + Host: config.Host, + ServiceName: config.ServiceName, + DisableSNI: config.DisableSNI, + ReduceRtt: config.ReduceRtt, + UDPRelayMode: config.UDPRelayMode, + CongestionController: config.CongestionController, + }, + Security: config.Security, + SecurityConfig: &SecurityConfig{ + SNI: config.SNI, + AllowInsecure: &config.AllowInsecure, + Fingerprint: config.Fingerprint, + RealityServerAddress: config.RealityServerAddr, + RealityServerPort: config.RealityServerPort, + RealityPrivateKey: config.RealityPrivateKey, + RealityPublicKey: config.RealityPublicKey, + RealityShortId: config.RealityShortId, + }, + } + case AnyTLS: + result = AnyTLSNode{ + Port: config.Port, + SecurityConfig: &SecurityConfig{ + SNI: config.SNI, + AllowInsecure: &config.AllowInsecure, + Fingerprint: config.Fingerprint, + RealityServerAddress: config.RealityServerAddr, + RealityServerPort: config.RealityServerPort, + RealityPrivateKey: config.RealityPrivateKey, + RealityPublicKey: config.RealityPublicKey, + RealityShortId: config.RealityShortId, + }, + } + case Tuic: + result = TuicNode{ + Port: config.Port, + SecurityConfig: &SecurityConfig{ + SNI: config.SNI, + AllowInsecure: &config.AllowInsecure, + Fingerprint: config.Fingerprint, + RealityServerAddress: config.RealityServerAddr, + RealityServerPort: config.RealityServerPort, + RealityPrivateKey: config.RealityPrivateKey, + RealityPublicKey: config.RealityPublicKey, + RealityShortId: config.RealityShortId, + }, + } + case Hysteria2: + result = Hysteria2Node{ + Port: config.Port, + HopPorts: config.HopPorts, + HopInterval: config.HopInterval, + ObfsPassword: config.ObfsPassword, + SecurityConfig: &SecurityConfig{ + SNI: config.SNI, + AllowInsecure: &config.AllowInsecure, + Fingerprint: config.Fingerprint, + RealityServerAddress: config.RealityServerAddr, + RealityServerPort: config.RealityServerPort, + RealityPrivateKey: config.RealityPrivateKey, + RealityPublicKey: config.RealityPublicKey, + RealityShortId: config.RealityShortId, + }, + } + + } + var resp map[string]interface{} + s, _ := json.Marshal(result) + _ = json.Unmarshal(s, &resp) + return resp +} diff --git a/internal/logic/server/getServerUserListLogic.go b/internal/logic/server/getServerUserListLogic.go index bf4a6a3..54966d1 100644 --- a/internal/logic/server/getServerUserListLogic.go +++ b/internal/logic/server/getServerUserListLogic.go @@ -3,8 +3,10 @@ package server import ( "encoding/json" "fmt" + "strings" "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/model/node" "github.com/perfect-panel/server/internal/config" "github.com/perfect-panel/server/internal/svc" @@ -33,28 +35,46 @@ func NewGetServerUserListLogic(ctx *gin.Context, svcCtx *svc.ServiceContext) *Ge func (l *GetServerUserListLogic) GetServerUserList(req *types.GetServerUserListRequest) (resp *types.GetServerUserListResponse, err error) { cacheKey := fmt.Sprintf("%s%d", config.ServerUserListCacheKey, req.ServerId) cache, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() - if err == nil { - if cache != "" { - etag := tool.GenerateETag([]byte(cache)) - resp := &types.GetServerUserListResponse{} - // Check If-None-Match header - if match := l.ctx.GetHeader("If-None-Match"); match == etag { - return nil, xerr.StatusNotModified - } - l.ctx.Header("ETag", etag) - err = json.Unmarshal([]byte(cache), resp) - if err != nil { - l.Errorw("[ServerUserListCacheKey] json unmarshal error", logger.Field("error", err.Error())) - return nil, err - } - return resp, nil + if cache != "" { + etag := tool.GenerateETag([]byte(cache)) + resp = &types.GetServerUserListResponse{} + // Check If-None-Match header + if match := l.ctx.GetHeader("If-None-Match"); match == etag { + return nil, xerr.StatusNotModified } + l.ctx.Header("ETag", etag) + err = json.Unmarshal([]byte(cache), resp) + if err != nil { + l.Errorw("[ServerUserListCacheKey] json unmarshal error", logger.Field("error", err.Error())) + return nil, err + } + return resp, nil } - server, err := l.svcCtx.ServerModel.FindOne(l.ctx, req.ServerId) + server, err := l.svcCtx.NodeModel.FindOneServer(l.ctx, req.ServerId) if err != nil { return nil, err } - subs, err := l.svcCtx.SubscribeModel.QuerySubscribeIdsByServerIdAndServerGroupId(l.ctx, server.Id, server.GroupId) + + _, nodes, err := l.svcCtx.NodeModel.FilterNodeList(l.ctx, &node.FilterNodeParams{ + Page: 1, + Size: 1000, + ServerId: []int64{server.Id}, + Protocol: req.Protocol, + }) + if err != nil { + l.Errorw("FilterNodeList error", logger.Field("error", err.Error())) + return nil, err + } + var nodeTag []string + var nodeIds []int64 + for _, n := range nodes { + nodeIds = append(nodeIds, n.Id) + if n.Tags != "" { + nodeTag = append(nodeTag, strings.Split(n.Tags, ",")...) + } + } + + subs, err := l.svcCtx.SubscribeModel.QuerySubscribeIdsByNodeIdAndNodeTag(l.ctx, nodeIds, nodeTag) if err != nil { l.Errorw("QuerySubscribeIdsByServerIdAndServerGroupId error", logger.Field("error", err.Error())) return nil, err @@ -76,16 +96,10 @@ func (l *GetServerUserListLogic) GetServerUserList(req *types.GetServerUserListR return nil, err } for _, datum := range data { - speedLimit := server.SpeedLimit - if (int(sub.SpeedLimit) < server.SpeedLimit && sub.SpeedLimit != 0) || - (int(sub.SpeedLimit) > server.SpeedLimit && sub.SpeedLimit == 0) { - speedLimit = int(sub.SpeedLimit) - } - users = append(users, types.ServerUser{ Id: datum.Id, UUID: datum.UUID, - SpeedLimit: int64(speedLimit), + SpeedLimit: sub.SpeedLimit, DeviceLimit: sub.DeviceLimit, }) } @@ -106,5 +120,9 @@ func (l *GetServerUserListLogic) GetServerUserList(req *types.GetServerUserListR if err != nil { l.Errorw("[ServerUserListCacheKey] redis set error", logger.Field("error", err.Error())) } + // Check If-None-Match header + if match := l.ctx.GetHeader("If-None-Match"); match == etag { + return nil, xerr.StatusNotModified + } return resp, nil } diff --git a/internal/logic/server/pushOnlineUsersLogic.go b/internal/logic/server/pushOnlineUsersLogic.go index b8ce501..9833e65 100644 --- a/internal/logic/server/pushOnlineUsersLogic.go +++ b/internal/logic/server/pushOnlineUsersLogic.go @@ -40,7 +40,7 @@ func (l *PushOnlineUsersLogic) PushOnlineUsers(req *types.OnlineUsersRequest) er } // Find server info - _, err := l.svcCtx.ServerModel.FindOne(l.ctx, req.ServerId) + _, err := l.svcCtx.NodeModel.FindOneServer(l.ctx, req.ServerId) if err != nil { l.Errorw("[PushOnlineUsers] FindOne error", logger.Field("error", err)) return fmt.Errorf("server not found: %w", err) diff --git a/internal/logic/server/serverPushStatusLogic.go b/internal/logic/server/serverPushStatusLogic.go index 7e5ff91..5d7b4e8 100644 --- a/internal/logic/server/serverPushStatusLogic.go +++ b/internal/logic/server/serverPushStatusLogic.go @@ -27,7 +27,7 @@ func NewServerPushStatusLogic(ctx context.Context, svcCtx *svc.ServiceContext) * func (l *ServerPushStatusLogic) ServerPushStatus(req *types.ServerPushStatusRequest) error { // Find server info - serverInfo, err := l.svcCtx.ServerModel.FindOne(l.ctx, req.ServerId) + serverInfo, err := l.svcCtx.NodeModel.FindOneServer(l.ctx, req.ServerId) if err != nil || serverInfo.Id <= 0 { l.Errorw("[PushOnlineUsers] FindOne error", logger.Field("error", err)) return errors.New("server not found") diff --git a/internal/logic/server/serverPushUserTrafficLogic.go b/internal/logic/server/serverPushUserTrafficLogic.go index 3481340..0ce5b32 100644 --- a/internal/logic/server/serverPushUserTrafficLogic.go +++ b/internal/logic/server/serverPushUserTrafficLogic.go @@ -32,7 +32,7 @@ func NewServerPushUserTrafficLogic(ctx context.Context, svcCtx *svc.ServiceConte func (l *ServerPushUserTrafficLogic) ServerPushUserTraffic(req *types.ServerPushUserTrafficRequest) error { // Find server info - serverInfo, err := l.svcCtx.ServerModel.FindOne(l.ctx, req.ServerId) + serverInfo, err := l.svcCtx.NodeModel.FindOneServer(l.ctx, req.ServerId) if err != nil { l.Errorw("[PushOnlineUsers] FindOne error", logger.Field("error", err)) return errors.New("server not found") diff --git a/internal/logic/subscribe/subscribeLogic.go b/internal/logic/subscribe/subscribeLogic.go index 9f8ee2c..4c64522 100644 --- a/internal/logic/subscribe/subscribeLogic.go +++ b/internal/logic/subscribe/subscribeLogic.go @@ -9,7 +9,7 @@ import ( "github.com/perfect-panel/server/adapter" "github.com/perfect-panel/server/internal/model/client" "github.com/perfect-panel/server/internal/model/log" - "github.com/perfect-panel/server/internal/model/server" + "github.com/perfect-panel/server/internal/model/node" "github.com/perfect-panel/server/internal/model/user" @@ -196,7 +196,7 @@ func (l *SubscribeLogic) logSubscribeActivity(subscribeStatus bool, userSub *use } } -func (l *SubscribeLogic) getServers(userSub *user.Subscribe) ([]*server.Server, error) { +func (l *SubscribeLogic) getServers(userSub *user.Subscribe) ([]*node.Node, error) { if l.isSubscriptionExpired(userSub) { return l.createExpiredServers(), nil } @@ -207,49 +207,61 @@ func (l *SubscribeLogic) getServers(userSub *user.Subscribe) ([]*server.Server, return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe details error: %v", err.Error()) } - serverIds := tool.StringToInt64Slice(subDetails.Server) - groupIds := tool.StringToInt64Slice(subDetails.ServerGroup) + nodeIds := tool.StringToInt64Slice(subDetails.Nodes) + tags := strings.Split(subDetails.NodeTags, ",") - l.Debugf("[Generate Subscribe]serverIds: %v, groupIds: %v", serverIds, groupIds) + l.Debugf("[Generate Subscribe]nodes: %v, NodeTags: %v", nodeIds, tags) - servers, err := l.svc.ServerModel.FindServerDetailByGroupIdsAndIds(l.ctx.Request.Context(), groupIds, serverIds) + _, nodes, err := l.svc.NodeModel.FilterNodeList(l.ctx.Request.Context(), &node.FilterNodeParams{ + Page: 1, + Size: 1000, + ServerId: nodeIds, + Tag: tags, + Preload: true, + }) - l.Debugf("[Query Subscribe]found servers: %v", len(servers)) + l.Debugf("[Query Subscribe]found servers: %v", len(nodes)) if err != nil { l.Errorw("[Generate Subscribe]find server details error: %v", logger.Field("error", err.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find server details error: %v", err.Error()) } - logger.Debugf("[Generate Subscribe]found servers: %v", len(servers)) - return servers, nil + logger.Debugf("[Generate Subscribe]found servers: %v", len(nodes)) + return nodes, nil } func (l *SubscribeLogic) isSubscriptionExpired(userSub *user.Subscribe) bool { return userSub.ExpireTime.Unix() < time.Now().Unix() && userSub.ExpireTime.Unix() != 0 } -func (l *SubscribeLogic) createExpiredServers() []*server.Server { +func (l *SubscribeLogic) createExpiredServers() []*node.Node { enable := true host := l.getFirstHostLine() - return []*server.Server{ + return []*node.Node{ { - Name: "Subscribe Expired", - ServerAddr: "127.0.0.1", - RelayMode: "none", - Protocol: "shadowsocks", - Config: "{\"method\":\"aes-256-gcm\",\"port\":1}", - Enable: &enable, - Sort: 0, + Name: "Subscribe Expired", + Tags: "", + Port: 18080, + Address: "127.0.0.1", + Server: &node.Server{ + Name: "Subscribe Expired", + Protocols: "[{\"type:\"\"shadowsocks\",\"cipher\":\"aes-256-gcm\",\"port\":1}]", + }, + Protocol: "shadowsocks", + Enabled: &enable, }, { - Name: host, - ServerAddr: "127.0.0.1", - RelayMode: "none", - Protocol: "shadowsocks", - Config: "{\"method\":\"aes-256-gcm\",\"port\":1}", - Enable: &enable, - Sort: 0, + Name: host, + Tags: "", + Port: 18080, + Address: "127.0.0.1", + Server: &node.Server{ + Name: "Subscribe Expired", + Protocols: "[{\"type:\"\"shadowsocks\",\"cipher\":\"aes-256-gcm\",\"port\":1}]", + }, + Protocol: "shadowsocks", + Enabled: &enable, }, } } diff --git a/internal/model/node/default.go b/internal/model/node/default.go index f0bac90..62fd9ff 100644 --- a/internal/model/node/default.go +++ b/internal/model/node/default.go @@ -3,6 +3,7 @@ package node import ( "context" + "github.com/redis/go-redis/v9" "gorm.io/gorm" ) @@ -35,19 +36,21 @@ type ( } defaultServerModel struct { *gorm.DB + Cache *redis.Client } ) -func newServerModel(db *gorm.DB) *defaultServerModel { +func newServerModel(db *gorm.DB, cache *redis.Client) *defaultServerModel { return &defaultServerModel{ - DB: db, + DB: db, + Cache: cache, } } // NewModel returns a model for the database table. -func NewModel(conn *gorm.DB) Model { +func NewModel(conn *gorm.DB, cache *redis.Client) Model { return &customServerModel{ - defaultServerModel: newServerModel(conn), + defaultServerModel: newServerModel(conn, cache), } } diff --git a/internal/model/node/model.go b/internal/model/node/model.go index 1aefb05..7af5399 100644 --- a/internal/model/node/model.go +++ b/internal/model/node/model.go @@ -1,12 +1,26 @@ package node -import "context" +import ( + "context" + "fmt" + + "github.com/perfect-panel/server/pkg/tool" +) type customServerLogicModel interface { FilterServerList(ctx context.Context, params *FilterParams) (int64, []*Server, error) - FilterNodeList(ctx context.Context, params *FilterParams) (int64, []*Node, error) + FilterNodeList(ctx context.Context, params *FilterNodeParams) (int64, []*Node, error) + ClearNodeCache(ctx context.Context, params *FilterNodeParams) error } +const ( + // ServerUserListCacheKey Server User List Cache Key + ServerUserListCacheKey = "server:user_list:id:" + + // ServerConfigCacheKey Server Config Cache Key + ServerConfigCacheKey = "server:config:id:" +) + // FilterParams Filter Server Params type FilterParams struct { Page int @@ -14,6 +28,16 @@ type FilterParams struct { Search string } +type FilterNodeParams struct { + Page int // Page Number + Size int // Page Size + ServerId []int64 // Server IDs + Tag []string // Tags + Search string // Search Address or Name + Protocol string // Protocol + Preload bool // Preload Server +} + // FilterServerList Filter Server List func (m *customServerModel) FilterServerList(ctx context.Context, params *FilterParams) (int64, []*Server, error) { var servers []*Server @@ -29,17 +53,18 @@ func (m *customServerModel) FilterServerList(ctx context.Context, params *Filter s := "%" + params.Search + "%" query = query.Where("`name` LIKE ? OR `address` LIKE ?", s, s) } + err := query.Count(&total).Limit(params.Size).Offset((params.Page - 1) * params.Size).Find(&servers).Error return total, servers, err } // FilterNodeList Filter Node List -func (m *customServerModel) FilterNodeList(ctx context.Context, params *FilterParams) (int64, []*Node, error) { +func (m *customServerModel) FilterNodeList(ctx context.Context, params *FilterNodeParams) (int64, []*Node, error) { var nodes []*Node var total int64 query := m.WithContext(ctx).Model(&Node{}) if params == nil { - params = &FilterParams{ + params = &FilterNodeParams{ Page: 1, Size: 10, } @@ -48,6 +73,40 @@ func (m *customServerModel) FilterNodeList(ctx context.Context, params *FilterPa s := "%" + params.Search + "%" query = query.Where("`name` LIKE ? OR `address` LIKE ? OR `tags` LIKE ? OR `port` LIKE ? ", s, s, s, s) } + if len(params.ServerId) > 0 { + query = query.Where("server_id IN ?", params.ServerId) + } + if len(params.Tag) > 0 { + for _, tag := range params.Tag { + query = query.Or("FIND_IN_SET(?,tags)", tag) + } + } + if params.Protocol != "" { + query = query.Where("protocol = ?", params.Protocol) + } + + if params.Preload { + query = query.Preload("Server") + } + err := query.Count(&total).Limit(params.Size).Offset((params.Page - 1) * params.Size).Find(&nodes).Error return total, nodes, err } + +// ClearNodeCache Clear Node Cache +func (m *customServerModel) ClearNodeCache(ctx context.Context, params *FilterNodeParams) error { + _, nodes, err := m.FilterNodeList(ctx, params) + if err != nil { + return err + } + var cacheKeys []string + for _, node := range nodes { + cacheKeys = append(cacheKeys, fmt.Sprintf("%s%d", ServerUserListCacheKey, node.ServerId), fmt.Sprintf("%s%d", ServerConfigCacheKey, node.ServerId)) + } + + if len(cacheKeys) > 0 { + cacheKeys = tool.RemoveDuplicateElements(cacheKeys...) + return m.Cache.Del(ctx, cacheKeys...).Err() + } + return nil +} diff --git a/internal/model/node/node.go b/internal/model/node/node.go index 6e5c35b..7ec6de5 100644 --- a/internal/model/node/node.go +++ b/internal/model/node/node.go @@ -9,6 +9,7 @@ type Node struct { Port uint16 `gorm:"not null;default:0;comment:Connect Port"` Address string `gorm:"type:varchar(255);not null;default:'';comment:Connect Address"` ServerId int64 `gorm:"not null;default:0;comment:Server ID"` + 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"` CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` diff --git a/internal/model/node/server.go b/internal/model/node/server.go index e5d59c6..e531a4a 100644 --- a/internal/model/node/server.go +++ b/internal/model/node/server.go @@ -8,17 +8,17 @@ import ( ) type Server struct { - Id int64 `gorm:"primary_key"` - Name string `gorm:"type:varchar(100);not null;default:'';comment:Server Name"` - Country string `gorm:"type:varchar(128);not null;default:'';comment:Country"` - City string `gorm:"type:varchar(128);not null;default:'';comment:City"` - Ratio float32 `gorm:"type:DECIMAL(4,2);not null;default:0;comment:Traffic Ratio"` - Address string `gorm:"type:varchar(100);not null;default:'';comment:Server Address"` - Sort int `gorm:"type:int;not null;default:0;comment:Sort"` - Protocols string `gorm:"type:text;default:null;comment:Protocol"` - LastReportedAt time.Time `gorm:"comment:Last Reported Time"` - CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` - UpdatedAt time.Time `gorm:"comment:Update Time"` + Id int64 `gorm:"primary_key"` + Name string `gorm:"type:varchar(100);not null;default:'';comment:Server Name"` + Country string `gorm:"type:varchar(128);not null;default:'';comment:Country"` + City string `gorm:"type:varchar(128);not null;default:'';comment:City"` + Ratio float32 `gorm:"type:DECIMAL(4,2);not null;default:0;comment:Traffic Ratio"` + Address string `gorm:"type:varchar(100);not null;default:'';comment:Server Address"` + Sort int `gorm:"type:int;not null;default:0;comment:Sort"` + Protocols string `gorm:"type:text;default:null;comment:Protocol"` + LastReportedAt *time.Time `gorm:"comment:Last Reported Time"` + CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` } func (*Server) TableName() string { diff --git a/internal/model/subscribe/default.go b/internal/model/subscribe/default.go index 92879d7..67f15d9 100644 --- a/internal/model/subscribe/default.go +++ b/internal/model/subscribe/default.go @@ -4,11 +4,7 @@ import ( "context" "errors" "fmt" - "strconv" - "strings" - "github.com/perfect-panel/server/internal/config" - "github.com/perfect-panel/server/internal/model/server" "github.com/perfect-panel/server/pkg/cache" "github.com/redis/go-redis/v9" "gorm.io/gorm" @@ -61,42 +57,7 @@ func (m *defaultSubscribeModel) getCacheKeys(data *Subscribe) []string { if data == nil { return []string{} } - SubscribeIdKey := fmt.Sprintf("%s%v", cacheSubscribeIdPrefix, data.Id) - serverKey := make([]string, 0) - if data.Server != "" { - cacheKey := strings.Split(data.Server, ",") - for _, v := range cacheKey { - if v != "" { - serverKey = append(serverKey, fmt.Sprintf("%s%v", config.ServerUserListCacheKey, v)) - } - } - } - // Temporary solution waiting for refactoring - if data.ServerGroup != "" { - cacheKey := strings.Split(data.ServerGroup, ",") - groupIds := make([]int64, 0) - for _, v := range cacheKey { - if v != "" { - id, _ := strconv.ParseInt(v, 10, 64) - if id > 0 { - groupIds = append(groupIds, id) - } - } - } - var ids []int64 - _ = m.Transaction(context.Background(), func(tx *gorm.DB) error { - return tx.Model(&server.Server{}).Where("group_id IN ?", groupIds).Pluck("id", &ids).Error - }) - for _, id := range ids { - serverKey = append(serverKey, fmt.Sprintf("%s%v", config.ServerUserListCacheKey, id)) - } - } - - cacheKeys := []string{SubscribeIdKey} - if len(serverKey) > 0 { - cacheKeys = append(cacheKeys, serverKey...) - } - return cacheKeys + return []string{fmt.Sprintf("%s%v", cacheSubscribeIdPrefix, data.Id)} } func (m *defaultSubscribeModel) Insert(ctx context.Context, data *Subscribe, tx ...*gorm.DB) error { diff --git a/internal/model/subscribe/model.go b/internal/model/subscribe/model.go index f652bc1..8d30c8d 100644 --- a/internal/model/subscribe/model.go +++ b/internal/model/subscribe/model.go @@ -7,32 +7,11 @@ import ( "gorm.io/gorm" ) -// type Details struct { -// Id int64 `gorm:"primaryKey"` -// Name string `gorm:"type:varchar(255);not null;default:'';comment:Subscribe Name"` -// Description string `gorm:"type:text;comment:Subscribe Description"` -// UnitPrice int64 `gorm:"type:int;not null;default:0;comment:Unit Price"` -// UnitTime string `gorm:"type:varchar(255);not null;default:'';comment:Unit Time"` -// Discount string `gorm:"type:text;comment:Discount"` -// Replacement int64 `gorm:"type:int;not null;default:0;comment:Replacement"` -// Inventory int64 `gorm:"type:int;not null;default:0;comment:Inventory"` -// Traffic int64 `gorm:"type:int;not null;default:0;comment:Traffic"` -// SpeedLimit int64 `gorm:"type:int;not null;default:0;comment:Speed Limit"` -// DeviceLimit int64 `gorm:"type:int;not null;default:0;comment:Device Limit"` -// GroupId int64 `gorm:"type:bigint;comment:Group Id"` -// Quota int64 `gorm:"type:int;not null;default:0;comment:Quota"` -// Show *bool `gorm:"type:tinyint(1);not null;default:0;comment:Show"` -// Sell *bool `gorm:"type:tinyint(1);not null;default:0;comment:Sell"` -// DeductionRatio int64 `gorm:"type:int;default:0;comment:Deduction Ratio"` -// PurchaseWithDiscount bool `gorm:"type:tinyint(1);default:0;comment:PurchaseWithDiscount"` -// ResetCycle int64 `gorm:"type:int;default:0;comment:Reset Cycle"` -// RenewalReset bool `gorm:"type:tinyint(1);default:0;comment:Renew Reset"` -// } type customSubscribeLogicModel interface { QuerySubscribeListByPage(ctx context.Context, page, size int, group int64, search string) (total int64, list []*Subscribe, err error) QuerySubscribeList(ctx context.Context) ([]*Subscribe, error) QuerySubscribeListByShow(ctx context.Context) ([]*Subscribe, error) - QuerySubscribeIdsByServerIdAndServerGroupId(ctx context.Context, serverId, serverGroupId int64) ([]*Subscribe, error) + QuerySubscribeIdsByNodeIdAndNodeTag(ctx context.Context, node []int64, tags []string) ([]*Subscribe, error) QuerySubscribeMinSortByIds(ctx context.Context, ids []int64) (int64, error) QuerySubscribeListByIds(ctx context.Context, ids []int64) ([]*Subscribe, error) ClearCache(ctx context.Context, id ...int64) error @@ -75,10 +54,24 @@ func (m *customSubscribeModel) QuerySubscribeList(ctx context.Context) ([]*Subsc return list, err } -func (m *customSubscribeModel) QuerySubscribeIdsByServerIdAndServerGroupId(ctx context.Context, serverId, serverGroupId int64) ([]*Subscribe, error) { +func (m *customSubscribeModel) QuerySubscribeIdsByNodeIdAndNodeTag(ctx context.Context, node []int64, tags []string) ([]*Subscribe, error) { var data []*Subscribe err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error { - return conn.Model(&Subscribe{}).Where("FIND_IN_SET(?, server)", serverId).Or("FIND_IN_SET(?, server_group)", serverGroupId).Find(v).Error + db := conn.Model(&Subscribe{}) + if len(node) > 0 { + for _, id := range node { + db = db.Or("FIND_IN_SET(?, nodes)", id) + } + } + + if len(tags) > 0 { + // 拼接多个 tag 条件 + for _, t := range tags { + db = db.Or("FIND_IN_SET(?, node_tags)", t) + } + } + + return db.Find(v).Error }) return data, err } diff --git a/internal/model/subscribe/subscribe.go b/internal/model/subscribe/subscribe.go index 4261715..9193425 100644 --- a/internal/model/subscribe/subscribe.go +++ b/internal/model/subscribe/subscribe.go @@ -20,8 +20,8 @@ type Subscribe struct { DeviceLimit int64 `gorm:"type:int;not null;default:0;comment:Device Limit"` Quota int64 `gorm:"type:int;not null;default:0;comment:Quota"` GroupId int64 `gorm:"type:bigint;comment:Group Id"` - ServerGroup string `gorm:"type:varchar(255);comment:Server Group"` - Server string `gorm:"type:varchar(255);comment:Server"` + Nodes string `gorm:"type:varchar(255);comment:Node Ids"` + NodeTags string `gorm:"type:varchar(255);comment:Node Tags"` Show *bool `gorm:"type:tinyint(1);not null;default:0;comment:Show portal page"` Sell *bool `gorm:"type:tinyint(1);not null;default:0;comment:Sell"` Sort int64 `gorm:"type:int;not null;default:0;comment:Sort"` diff --git a/internal/svc/serviceContext.go b/internal/svc/serviceContext.go index d701590..f2c224c 100644 --- a/internal/svc/serviceContext.go +++ b/internal/svc/serviceContext.go @@ -18,7 +18,6 @@ import ( "github.com/perfect-panel/server/internal/model/log" "github.com/perfect-panel/server/internal/model/order" "github.com/perfect-panel/server/internal/model/payment" - "github.com/perfect-panel/server/internal/model/server" "github.com/perfect-panel/server/internal/model/subscribe" "github.com/perfect-panel/server/internal/model/subscribeType" "github.com/perfect-panel/server/internal/model/system" @@ -36,20 +35,20 @@ import ( ) type ServiceContext struct { - DB *gorm.DB - Redis *redis.Client - Config config.Config - Queue *asynq.Client - NodeCache *cache.NodeCacheClient - AuthModel auth.Model - AdsModel ads.Model - LogModel log.Model - NodeModel node.Model - UserModel user.Model - OrderModel order.Model - ClientModel client.Model - TicketModel ticket.Model - ServerModel server.Model + DB *gorm.DB + Redis *redis.Client + Config config.Config + Queue *asynq.Client + NodeCache *cache.NodeCacheClient + AuthModel auth.Model + AdsModel ads.Model + LogModel log.Model + NodeModel node.Model + UserModel user.Model + OrderModel order.Model + ClientModel client.Model + TicketModel ticket.Model + //ServerModel server.Model SystemModel system.Model CouponModel coupon.Model PaymentModel payment.Model @@ -87,21 +86,21 @@ func NewServiceContext(c config.Config) *ServiceContext { } authLimiter := limit.NewPeriodLimit(86400, 15, rds, config.SendCountLimitKeyPrefix, limit.Align()) srv := &ServiceContext{ - DB: db, - Redis: rds, - Config: c, - Queue: NewAsynqClient(c), - NodeCache: cache.NewNodeCacheClient(rds), - AuthLimiter: authLimiter, - AdsModel: ads.NewModel(db, rds), - LogModel: log.NewModel(db), - NodeModel: node.NewModel(db), - AuthModel: auth.NewModel(db, rds), - UserModel: user.NewModel(db, rds), - OrderModel: order.NewModel(db, rds), - ClientModel: client.NewSubscribeApplicationModel(db), - TicketModel: ticket.NewModel(db, rds), - ServerModel: server.NewModel(db, rds), + DB: db, + Redis: rds, + Config: c, + Queue: NewAsynqClient(c), + NodeCache: cache.NewNodeCacheClient(rds), + AuthLimiter: authLimiter, + AdsModel: ads.NewModel(db, rds), + LogModel: log.NewModel(db), + NodeModel: node.NewModel(db, rds), + AuthModel: auth.NewModel(db, rds), + UserModel: user.NewModel(db, rds), + OrderModel: order.NewModel(db, rds), + ClientModel: client.NewSubscribeApplicationModel(db), + TicketModel: ticket.NewModel(db, rds), + //ServerModel: server.NewModel(db, rds), SystemModel: system.NewModel(db, rds), CouponModel: coupon.NewModel(db, rds), PaymentModel: payment.NewModel(db, rds), diff --git a/internal/types/types.go b/internal/types/types.go index 5cb6d3c..e0a2e32 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -384,8 +384,8 @@ type CreateSubscribeRequest struct { DeviceLimit int64 `json:"device_limit"` Quota int64 `json:"quota"` GroupId int64 `json:"group_id"` - ServerGroup []int64 `json:"server_group"` - Server []int64 `json:"server"` + Nodes []int64 `json:"nodes"` + NodeTags []string `json:"node_tags"` Show *bool `json:"show"` Sell *bool `json:"sell"` DeductionRatio int64 `json:"deduction_ratio"` @@ -1117,6 +1117,10 @@ type GoogleLoginCallbackRequest struct { State string `form:"state"` } +type HasMigrateSeverNodeResponse struct { + HasMigrate bool `json:"has_migrate"` +} + type Hysteria2 struct { Port int `json:"port" validate:"required"` HopPorts string `json:"hop_ports" validate:"required"` @@ -1163,6 +1167,12 @@ type MessageLog struct { CreatedAt int64 `json:"created_at"` } +type MigrateServerNodeResponse struct { + Succee uint64 `json:"succee"` + Fail uint64 `json:"fail"` + Message string `json:"message,omitempty"` +} + type MobileAuthenticateConfig struct { Enable bool `json:"enable"` EnableWhitelist bool `json:"enable_whitelist"` @@ -1825,8 +1835,8 @@ type Subscribe struct { DeviceLimit int64 `json:"device_limit"` Quota int64 `json:"quota"` GroupId int64 `json:"group_id"` - ServerGroup []int64 `json:"server_group"` - Server []int64 `json:"server"` + Nodes []int64 `json:"nodes"` + NodeTags []string `json:"node_tags"` Show bool `json:"show"` Sell bool `json:"sell"` Sort int64 `json:"sort"` @@ -2195,8 +2205,8 @@ type UpdateSubscribeRequest struct { DeviceLimit int64 `json:"device_limit"` Quota int64 `json:"quota"` GroupId int64 `json:"group_id"` - ServerGroup []int64 `json:"server_group"` - Server []int64 `json:"server"` + Nodes []int64 `json:"nodes"` + NodeTags []string `json:"node_tags"` Show *bool `json:"show"` Sell *bool `json:"sell"` Sort int64 `json:"sort"` diff --git a/pkg/tool/slice.go b/pkg/tool/slice.go index 9eef3fc..3797878 100644 --- a/pkg/tool/slice.go +++ b/pkg/tool/slice.go @@ -59,6 +59,7 @@ func Int64SliceToString(intSlice []int64) string { // string slice to string func StringSliceToString(stringSlice []string) string { + stringSlice = RemoveDuplicateElements(stringSlice...) return strings.Join(stringSlice, ",") } diff --git a/queue/logic/country/getCountryLogic.go b/queue/logic/country/getCountryLogic.go index 2b7ac41..75e0f6f 100644 --- a/queue/logic/country/getCountryLogic.go +++ b/queue/logic/country/getCountryLogic.go @@ -2,14 +2,9 @@ package countrylogic import ( "context" - "encoding/json" - - "github.com/perfect-panel/server/pkg/logger" "github.com/hibiken/asynq" "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/pkg/ip" - "github.com/perfect-panel/server/queue/types" ) type GetNodeCountryLogic struct { @@ -22,39 +17,6 @@ func NewGetNodeCountryLogic(svcCtx *svc.ServiceContext) *GetNodeCountryLogic { } } func (l *GetNodeCountryLogic) ProcessTask(ctx context.Context, task *asynq.Task) error { - var payload types.GetNodeCountry - if err := json.Unmarshal(task.Payload(), &payload); err != nil { - logger.WithContext(ctx).Error("[GetNodeCountryLogic] Unmarshal payload failed", - logger.Field("error", err.Error()), - logger.Field("payload", task.Payload()), - ) - return nil - } - serverAddr := payload.ServerAddr - resp, err := ip.GetRegionByIp(serverAddr) - if err != nil { - logger.WithContext(ctx).Error("[GetNodeCountryLogic] ", logger.Field("error", err.Error()), logger.Field("serverAddr", serverAddr)) - return nil - } - servers, err := l.svcCtx.ServerModel.FindNodeByServerAddrAndProtocol(ctx, payload.ServerAddr, payload.Protocol) - if err != nil { - logger.WithContext(ctx).Error("[GetNodeCountryLogic] FindNodeByServerAddrAnd", logger.Field("error", err.Error()), logger.Field("serverAddr", serverAddr)) - return err - } - if len(servers) == 0 { - return nil - } - for _, ser := range servers { - ser.Country = resp.Country - ser.City = resp.City - ser.Latitude = resp.Latitude - ser.Longitude = resp.Longitude - err := l.svcCtx.ServerModel.Update(ctx, ser) - if err != nil { - logger.WithContext(ctx).Error("[GetNodeCountryLogic] ", logger.Field("error", err.Error()), logger.Field("id", ser.Id)) - } - } - logger.WithContext(ctx).Info("[GetNodeCountryLogic] ", logger.Field("country", resp.Country), logger.Field("city", resp.Country)) return nil } diff --git a/queue/logic/order/activateOrderLogic.go b/queue/logic/order/activateOrderLogic.go index bb02e4c..041ca7b 100644 --- a/queue/logic/order/activateOrderLogic.go +++ b/queue/logic/order/activateOrderLogic.go @@ -7,16 +7,17 @@ import ( "encoding/json" "fmt" "strconv" + "strings" "time" "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/model/node" "github.com/perfect-panel/server/pkg/constant" "github.com/perfect-panel/server/pkg/logger" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" "github.com/google/uuid" "github.com/hibiken/asynq" - "github.com/perfect-panel/server/internal/config" "github.com/perfect-panel/server/internal/logic/telegram" "github.com/perfect-panel/server/internal/model/order" "github.com/perfect-panel/server/internal/model/subscribe" @@ -429,34 +430,18 @@ func (l *ActivateOrderLogic) calculateCommission(price int64) int64 { // clearServerCache clears user list cache for all servers associated with the subscription func (l *ActivateOrderLogic) clearServerCache(ctx context.Context, sub *subscribe.Subscribe) { - serverIds := tool.StringToInt64Slice(sub.Server) - groupServerIds := l.getServerIdsByGroups(ctx, sub.ServerGroup) - allServerIds := append(serverIds, groupServerIds...) + nodeIds := tool.StringToInt64Slice(sub.Nodes) + tags := strings.Split(sub.NodeTags, ",") - for _, id := range allServerIds { - cacheKey := fmt.Sprintf("%s%d", config.ServerUserListCacheKey, id) - if err := l.svc.Redis.Del(ctx, cacheKey).Err(); err != nil { - logger.WithContext(ctx).Error("Del server user list cache failed", - logger.Field("error", err.Error()), - logger.Field("cache_key", cacheKey), - ) - } - } -} - -// getServerIdsByGroups retrieves server IDs from server groups -func (l *ActivateOrderLogic) getServerIdsByGroups(ctx context.Context, serverGroup string) []int64 { - data, err := l.svc.ServerModel.FindServerListByGroupIds(ctx, tool.StringToInt64Slice(serverGroup)) + err := l.svc.NodeModel.ClearNodeCache(ctx, &node.FilterNodeParams{ + Page: 1, + Size: 1000, + ServerId: nodeIds, + Tag: tags, + }) if err != nil { - logger.WithContext(ctx).Error("Find server list failed", logger.Field("error", err.Error())) - return nil + logger.WithContext(ctx).Error("[Order Queue] Clear node cache failed", logger.Field("error", err.Error())) } - - serverIds := make([]int64, len(data)) - for i, item := range data { - serverIds[i] = item.Id - } - return serverIds } // Renewal handles subscription renewal including subscription extension, diff --git a/queue/logic/traffic/serverDataLogic.go b/queue/logic/traffic/serverDataLogic.go index 98fbaba..f8ca675 100644 --- a/queue/logic/traffic/serverDataLogic.go +++ b/queue/logic/traffic/serverDataLogic.go @@ -73,7 +73,7 @@ func (l *ServerDataLogic) getRanking(ctx context.Context) (top10ServerToday, top if s.ServerId == 0 { continue } - serverInfo, err := l.svc.ServerModel.FindOne(ctx, s.ServerId) + serverInfo, err := l.svc.NodeModel.FindOneServer(ctx, s.ServerId) if err != nil { logger.Error("[ServerDataLogic] Find server failed", logger.Field("error", err.Error())) continue @@ -92,7 +92,7 @@ func (l *ServerDataLogic) getRanking(ctx context.Context) (top10ServerToday, top logger.Error("[ServerDataLogic] Get top servers traffic by day failed", logger.Field("error", err.Error())) } else { for _, s := range serverYesterday { - serverInfo, err := l.svc.ServerModel.FindOne(ctx, s.ServerId) + serverInfo, err := l.svc.NodeModel.FindOneServer(ctx, s.ServerId) if err != nil { logger.Error("[ServerDataLogic] Find server failed", logger.Field("error", err.Error())) continue diff --git a/queue/logic/traffic/trafficStatisticsLogic.go b/queue/logic/traffic/trafficStatisticsLogic.go index 07509f0..1d3dfa6 100644 --- a/queue/logic/traffic/trafficStatisticsLogic.go +++ b/queue/logic/traffic/trafficStatisticsLogic.go @@ -38,7 +38,7 @@ func (l *TrafficStatisticsLogic) ProcessTask(ctx context.Context, task *asynq.Ta return nil } // query server info - serverInfo, err := l.svc.ServerModel.FindOne(ctx, payload.ServerId) + serverInfo, err := l.svc.NodeModel.FindOneServer(ctx, payload.ServerId) if err != nil { logger.WithContext(ctx).Error("[TrafficStatistics] Find server info failed", logger.Field("serverId", payload.ServerId), @@ -46,23 +46,22 @@ func (l *TrafficStatisticsLogic) ProcessTask(ctx context.Context, task *asynq.Ta ) return nil } - if serverInfo.TrafficRatio == 0 { - logger.WithContext(ctx).Error("[TrafficStatistics] Server log ratio is 0", - logger.Field("serverId", payload.ServerId), - ) - return nil + var serverRatio float32 = 1.0 + if serverInfo.Ratio > 0 { + serverRatio = serverInfo.Ratio } + now := time.Now() realTimeMultiplier := l.svc.NodeMultiplierManager.GetMultiplier(now) for _, log := range payload.Logs { // update user subscribe with log - d := int64(float32(log.Download) * serverInfo.TrafficRatio * realTimeMultiplier) - u := int64(float32(log.Upload) * serverInfo.TrafficRatio * realTimeMultiplier) + d := int64(float32(log.Download) * serverRatio * realTimeMultiplier) + u := int64(float32(log.Upload) * serverRatio * realTimeMultiplier) if err := l.svc.UserModel.UpdateUserSubscribeWithTraffic(ctx, log.SID, d, u); err != nil { logger.WithContext(ctx).Error("[TrafficStatistics] Update user subscribe with log failed", logger.Field("sid", log.SID), - logger.Field("download", float32(log.Download)*serverInfo.TrafficRatio), - logger.Field("upload", float32(log.Upload)*serverInfo.TrafficRatio), + logger.Field("download", float32(log.Download)*serverRatio), + logger.Field("upload", float32(log.Upload)*serverRatio), logger.Field("error", err.Error()), ) continue @@ -88,8 +87,8 @@ func (l *TrafficStatisticsLogic) ProcessTask(ctx context.Context, task *asynq.Ta }); err != nil { logger.WithContext(ctx).Error("[TrafficStatistics] Create log log failed", logger.Field("uid", log.SID), - logger.Field("download", float32(log.Download)*serverInfo.TrafficRatio), - logger.Field("upload", float32(log.Upload)*serverInfo.TrafficRatio), + logger.Field("download", float32(log.Download)*serverRatio), + logger.Field("upload", float32(log.Upload)*serverRatio), logger.Field("error", err.Error()), ) }