feat(api): migrate server and node data handling, update related structures and logic

This commit is contained in:
Chang lue Tsen 2025-08-26 07:05:59 -04:00
parent 9b3cdbbb4f
commit c7884d94aa
52 changed files with 1079 additions and 458 deletions

View File

@ -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
}

View File

@ -13,7 +13,7 @@ import (
type Proxy struct {
Name string
Server string
Port uint64
Port uint16
Type string
Tags []string

View File

@ -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
}

View File

@ -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)
}

View File

@ -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"`

View File

@ -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"`

View File

@ -1 +1,2 @@
DROP TABLE IF EXISTS `servers`;
DROP TABLE IF EXISTS `nodes`;
DROP TABLE IF EXISTS `servers`;

View File

@ -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;
) 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;

View File

@ -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';

View File

@ -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`;

View File

@ -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")
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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))

View File

@ -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())

View File

@ -0,0 +1,11 @@
package server
const (
ShadowSocks = "shadowsocks"
Vmess = "vmess"
Vless = "vless"
Trojan = "trojan"
AnyTLS = "anytls"
Tuic = "tuic"
Hysteria2 = "hysteria2"
)

View File

@ -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,

View File

@ -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{

View File

@ -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
}

View File

@ -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
}

View File

@ -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,

View File

@ -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
}

View File

@ -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)
}

View File

@ -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,

View File

@ -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"),

View File

@ -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"),

View File

@ -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"),

View File

@ -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"),

View File

@ -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,

View File

@ -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"),

View File

@ -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"`
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)

View File

@ -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")

View File

@ -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")

View File

@ -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,
},
}
}

View File

@ -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),
}
}

View File

@ -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
}

View File

@ -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"`

View File

@ -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 {

View File

@ -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 {

View File

@ -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
}

View File

@ -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"`

View File

@ -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),

View File

@ -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"`

View File

@ -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, ",")
}

View File

@ -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
}

View File

@ -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,

View File

@ -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

View File

@ -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()),
)
}