From d8e2e8168880e2e461648d44b1d6fb508019ec56 Mon Sep 17 00:00:00 2001 From: lain Date: Thu, 23 Oct 2025 08:47:11 -0300 Subject: [PATCH 01/50] add user subscribe note --- .../02119_user_subscribe_note.down.sql | 2 + .../database/02119_user_subscribe_note.up.sql | 4 + .../user/updateUserSubscribeNoteHandler.go | 26 +++++++ internal/handler/routes.go | 3 + .../user/updateUserSubscribeNoteLogic.go | 73 +++++++++++++++++++ internal/model/user/model.go | 1 + internal/model/user/user.go | 1 + internal/types/types.go | 8 ++ 8 files changed, 118 insertions(+) create mode 100644 initialize/migrate/database/02119_user_subscribe_note.down.sql create mode 100644 initialize/migrate/database/02119_user_subscribe_note.up.sql create mode 100644 internal/handler/public/user/updateUserSubscribeNoteHandler.go create mode 100644 internal/logic/public/user/updateUserSubscribeNoteLogic.go diff --git a/initialize/migrate/database/02119_user_subscribe_note.down.sql b/initialize/migrate/database/02119_user_subscribe_note.down.sql new file mode 100644 index 0000000..60cc0e8 --- /dev/null +++ b/initialize/migrate/database/02119_user_subscribe_note.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE `user_subscribe` +DROP COLUMN `note`; diff --git a/initialize/migrate/database/02119_user_subscribe_note.up.sql b/initialize/migrate/database/02119_user_subscribe_note.up.sql new file mode 100644 index 0000000..b8b6983 --- /dev/null +++ b/initialize/migrate/database/02119_user_subscribe_note.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE `user_subscribe` +ADD COLUMN `note` VARCHAR(500) NOT NULL DEFAULT '' + COMMENT 'User note for subscription' + AFTER `status`; diff --git a/internal/handler/public/user/updateUserSubscribeNoteHandler.go b/internal/handler/public/user/updateUserSubscribeNoteHandler.go new file mode 100644 index 0000000..17b77bf --- /dev/null +++ b/internal/handler/public/user/updateUserSubscribeNoteHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/public/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Update User Subscribe Note +func UpdateUserSubscribeNoteHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UpdateUserSubscribeNoteRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewUpdateUserSubscribeNoteLogic(c.Request.Context(), svcCtx) + err := l.UpdateUserSubscribeNote(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/routes.go b/internal/handler/routes.go index d42ac7e..943ba84 100644 --- a/internal/handler/routes.go +++ b/internal/handler/routes.go @@ -822,6 +822,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Reset User Subscribe Token publicUserGroupRouter.PUT("/subscribe_token", publicUser.ResetUserSubscribeTokenHandler(serverCtx)) + // Update User Subscribe Note + publicUserGroupRouter.PUT("/subscribe_note", publicUser.UpdateUserSubscribeNoteHandler(serverCtx)) + // Unbind Device publicUserGroupRouter.PUT("/unbind_device", publicUser.UnbindDeviceHandler(serverCtx)) diff --git a/internal/logic/public/user/updateUserSubscribeNoteLogic.go b/internal/logic/public/user/updateUserSubscribeNoteLogic.go new file mode 100644 index 0000000..3c43a8d --- /dev/null +++ b/internal/logic/public/user/updateUserSubscribeNoteLogic.go @@ -0,0 +1,73 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/server/pkg/constant" + + "github.com/perfect-panel/server/internal/model/user" + "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/tool" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type UpdateUserSubscribeNoteLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewUpdateUserSubscribeNoteLogic Update User Subscribe Note +func NewUpdateUserSubscribeNoteLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateUserSubscribeNoteLogic { + return &UpdateUserSubscribeNoteLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateUserSubscribeNoteLogic) UpdateUserSubscribeNote(req *types.UpdateUserSubscribeNoteRequest) error { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + + userSub, err := l.svcCtx.UserModel.FindOneUserSubscribe(l.ctx, req.UserSubscribeId) + if err != nil { + l.Errorw("FindOneUserSubscribe failed:", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindOneUserSubscribe failed: %v", err.Error()) + } + + if userSub.UserId != u.Id { + l.Errorw("UserSubscribeId does not belong to the current user") + return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "UserSubscribeId does not belong to the current user") + } + + userSub.Note = req.Note + var newSub user.Subscribe + tool.DeepCopy(&newSub, userSub) + + err = l.svcCtx.UserModel.UpdateSubscribe(l.ctx, &newSub) + if err != nil { + l.Errorw("UpdateSubscribe failed:", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "UpdateSubscribe failed: %v", err.Error()) + } + + // Clear user subscription cache + if err = l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, &newSub); err != nil { + l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("userSubscribeId", userSub.Id)) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "ClearSubscribeCache failed: %v", err.Error()) + } + + // Clear subscription cache + if err = l.svcCtx.SubscribeModel.ClearCache(l.ctx, userSub.SubscribeId); err != nil { + l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("subscribeId", userSub.SubscribeId)) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "ClearSubscribeCache failed: %v", err.Error()) + } + + return nil +} diff --git a/internal/model/user/model.go b/internal/model/user/model.go index ffc5020..86caa0d 100644 --- a/internal/model/user/model.go +++ b/internal/model/user/model.go @@ -36,6 +36,7 @@ type SubscribeDetails struct { Token string `gorm:"index:idx_token;unique;type:varchar(255);default:'';comment:Token"` UUID string `gorm:"type:varchar(255);unique;index:idx_uuid;default:'';comment:UUID"` Status uint8 `gorm:"type:tinyint(1);default:0;comment:Subscription Status: 0: Pending 1: Active 2: Finished 3: Expired; 4: Cancelled"` + Note string `gorm:"type:varchar(500);default:'';comment:User note for subscription"` CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` UpdatedAt time.Time `gorm:"comment:Update Time"` } diff --git a/internal/model/user/user.go b/internal/model/user/user.go index 98bfbcb..3344745 100644 --- a/internal/model/user/user.go +++ b/internal/model/user/user.go @@ -48,6 +48,7 @@ type Subscribe struct { Token string `gorm:"index:idx_token;unique;type:varchar(255);default:'';comment:Token"` UUID string `gorm:"type:varchar(255);unique;index:idx_uuid;default:'';comment:UUID"` Status uint8 `gorm:"type:tinyint(1);default:0;comment:Subscription Status: 0: Pending 1: Active 2: Finished 3: Expired 4: Deducted"` + Note string `gorm:"type:varchar(500);default:'';comment:User note for subscription"` CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` UpdatedAt time.Time `gorm:"comment:Update Time"` } diff --git a/internal/types/types.go b/internal/types/types.go index 4e481ff..9611bac 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -1804,6 +1804,11 @@ type ResetUserSubscribeTokenRequest struct { UserSubscribeId int64 `json:"user_subscribe_id"` } +type UpdateUserSubscribeNoteRequest struct { + UserSubscribeId int64 `json:"user_subscribe_id" validate:"required"` + Note string `json:"note" validate:"max=500"` +} + type RevenueStatisticsResponse struct { Today OrdersStatistics `json:"today"` Monthly OrdersStatistics `json:"monthly"` @@ -2576,6 +2581,7 @@ type UserSubscribe struct { Upload int64 `json:"upload"` Token string `json:"token"` Status uint8 `json:"status"` + Note string `json:"note"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` } @@ -2595,6 +2601,7 @@ type UserSubscribeDetail struct { Upload int64 `json:"upload"` Token string `json:"token"` Status uint8 `json:"status"` + Note string `json:"note"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` } @@ -2613,6 +2620,7 @@ type UserSubscribeInfo struct { Upload int64 `json:"upload"` Token string `json:"token"` Status uint8 `json:"status"` + Note string `json:"note"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` IsTryOut bool `json:"is_try_out"` From 2ed379d5e8307456b695b814e246511111c05c1d Mon Sep 17 00:00:00 2001 From: Tension Date: Sat, 1 Nov 2025 16:05:56 +0800 Subject: [PATCH 02/50] feat(report): add module registration and port management functionality --- ...ld github.com_perfect-panel_server.run.xml | 12 ----- internal/report/report.go | 22 ++++++++ internal/report/tool.go | 51 +++++++++++++++++++ internal/report/tool_test.go | 21 ++++++++ 4 files changed, 94 insertions(+), 12 deletions(-) delete mode 100644 .run/go build github.com_perfect-panel_server.run.xml create mode 100644 internal/report/report.go create mode 100644 internal/report/tool.go create mode 100644 internal/report/tool_test.go diff --git a/.run/go build github.com_perfect-panel_server.run.xml b/.run/go build github.com_perfect-panel_server.run.xml deleted file mode 100644 index 608afe7..0000000 --- a/.run/go build github.com_perfect-panel_server.run.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/internal/report/report.go b/internal/report/report.go new file mode 100644 index 0000000..31768e4 --- /dev/null +++ b/internal/report/report.go @@ -0,0 +1,22 @@ +package report + +const ( + GatewayURL = "http://127.0.0.1:%d" // 网关地址 + RegisterAPI = "/basic/register" // 模块注册接口 +) + +// RegisterRequest 模块注册请求参数 +type RegisterRequest struct { + Secret string `json:"secret"` // 通讯密钥 + ProxyPath string `json:"proxy_path"` // 代理路径 + ServiceURL string `json:"service_url"` // 服务地址 + Repository string `json:"repository"` // 服务代码仓库 + ServiceName string `json:"service_name"` // 服务名称 + ServiceVersion string `json:"service_version"` // 服务版本 +} + +// RegisterResponse 模块注册响应参数 +type RegisterResponse struct { + Success bool `json:"success"` // 注册是否成功 + Message string `json:"message"` // 返回信息 +} diff --git a/internal/report/tool.go b/internal/report/tool.go new file mode 100644 index 0000000..76894c2 --- /dev/null +++ b/internal/report/tool.go @@ -0,0 +1,51 @@ +package report + +import ( + "fmt" + "net" + "os" + + "github.com/pkg/errors" +) + +// FreePort returns a free TCP port by opening a listener on port 0. +func FreePort() (int, error) { + l, err := net.Listen("tcp", ":0") + if err != nil { + return 0, err + } + defer l.Close() + // Get the assigned port + addr := l.Addr().(*net.TCPAddr) + return addr.Port, nil +} + +// ModulePort returns the module port from the environment variable or a free port. +func ModulePort() (int, error) { + // 从环境变量获取端口号 + value, exists := os.LookupEnv("PPANEL_PORT") + if exists { + var port int + _, err := fmt.Sscanf(value, "%d", &port) + if err != nil { + return FreePort() + } + return port, nil + } + return FreePort() +} + +// GatewayPort returns the gateway port from the environment variable or a free port. +func GatewayPort() (int, error) { + // 从环境变量获取端口号 + value, exists := os.LookupEnv("GATEWAY_PORT") + if exists { + var port int + _, err := fmt.Sscanf(value, "%d", &port) + if err != nil { + panic(err) + } + return port, nil + } + return 0, errors.New("could not determine gateway port") +} diff --git a/internal/report/tool_test.go b/internal/report/tool_test.go new file mode 100644 index 0000000..cee9d83 --- /dev/null +++ b/internal/report/tool_test.go @@ -0,0 +1,21 @@ +package report + +import ( + "testing" +) + +func TestFreePort(t *testing.T) { + port, err := FreePort() + if err != nil { + t.Fatalf("FreePort() error: %v", err) + } + t.Logf("FreePort: %v", port) +} + +func TestModulePort(t *testing.T) { + port, err := ModulePort() + if err != nil { + t.Fatalf("ModulePort() error: %v", err) + } + t.Logf("ModulePort: %v", port) +} From a9c98b67f16361e71bdd928aa6f3517d791fb807 Mon Sep 17 00:00:00 2001 From: Ember Moth Date: Sun, 2 Nov 2025 15:02:15 +0800 Subject: [PATCH 03/50] add missing proxy field mappings --- adapter/adapter.go | 72 +++++++++++++++++++++++++++------------------- 1 file changed, 43 insertions(+), 29 deletions(-) diff --git a/adapter/adapter.go b/adapter/adapter.go index 1acc674..1d6e4f4 100644 --- a/adapter/adapter.go +++ b/adapter/adapter.go @@ -102,35 +102,49 @@ func (adapter *Adapter) Proxies(servers []*node.Node) ([]Proxy, error) { for _, protocol := range protocols { if protocol.Type == item.Protocol { proxies = append(proxies, Proxy{ - Sort: item.Sort, - 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, + Sort: item.Sort, + 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, + UpMbps: protocol.UpMbps, + DownMbps: protocol.DownMbps, + PaddingScheme: protocol.PaddingScheme, + Multiplex: protocol.Multiplex, + XhttpMode: protocol.XhttpMode, + XhttpExtra: protocol.XhttpExtra, + Encryption: protocol.Encryption, + EncryptionMode: protocol.EncryptionMode, + EncryptionRtt: protocol.EncryptionRtt, + EncryptionTicket: protocol.EncryptionTicket, + EncryptionServerPadding: protocol.EncryptionServerPadding, + EncryptionPrivateKey: protocol.EncryptionPrivateKey, + EncryptionClientPadding: protocol.EncryptionClientPadding, + EncryptionPassword: protocol.EncryptionPassword, }) } } From eb250956235c4e7c9e073dca567999344420612f Mon Sep 17 00:00:00 2001 From: Chang lue Tsen Date: Mon, 3 Nov 2025 14:12:27 -0500 Subject: [PATCH 04/50] feat(report): implement module registration and gateway mode handling --- internal/report/tool.go | 58 ++++++++++++++++++++++++++++++++++++++++ internal/report/types.go | 16 +++++++++++ internal/server.go | 26 +++++++++++++++++- 3 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 internal/report/types.go diff --git a/internal/report/tool.go b/internal/report/tool.go index 76894c2..25438cc 100644 --- a/internal/report/tool.go +++ b/internal/report/tool.go @@ -5,6 +5,9 @@ import ( "net" "os" + "github.com/go-resty/resty/v2" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/logger" "github.com/pkg/errors" ) @@ -43,9 +46,64 @@ func GatewayPort() (int, error) { var port int _, err := fmt.Sscanf(value, "%d", &port) if err != nil { + logger.Errorf("Failed to parse GATEWAY_PORT: %v Value %s", err.Error(), value) panic(err) } return port, nil } return 0, errors.New("could not determine gateway port") } + +// RegisterModule registers a module with the gateway. +func RegisterModule(port int) error { + // 从环境变量中读取网关模块端口 + gatewayPort, err := GatewayPort() + if err != nil { + return err + } + + // 从环境变量中获取通讯密钥 + value, exists := os.LookupEnv("SECRET_KEY") + if !exists { + panic("could not determine secret key") + } + + var response RegisterResponse + + client := resty.New().SetBaseURL(fmt.Sprintf("http://127.0.0.1:%d", gatewayPort)) + result, err := client.R().SetHeader("Content-Type", "application/json").SetBody(RegisterServiceRequest{ + Secret: value, + ProxyPath: "/api", + ServiceURL: fmt.Sprintf("http://127.0.0.1:%d", port), + Repository: "https://github.com/perfect-panel/server", + ServiceName: "ApiService", + ServiceVersion: constant.Version, + }).SetResult(&response).Post(RegisterAPI) + + if err != nil { + return err + } + + if result.IsError() { + return errors.New("failed to register module: " + result.Status()) + } + + if !response.Success { + return errors.New("failed to register module: " + response.Message) + } + logger.Infof("Module registered successfully: %s", response.Message) + return nil +} + +// IsGatewayMode checks if the application is running in gateway mode. +// It returns true if GATEWAY_MODE is set to "true" and GATEWAY_PORT is valid. +func IsGatewayMode() bool { + value, exists := os.LookupEnv("GATEWAY_MODE") + if exists && value == "true" { + if _, err := GatewayPort(); err == nil { + return true + } + } + + return false +} diff --git a/internal/report/types.go b/internal/report/types.go new file mode 100644 index 0000000..51d3c1a --- /dev/null +++ b/internal/report/types.go @@ -0,0 +1,16 @@ +package report + +// RegisterServiceResponse 模块注册请求参数 +type RegisterServiceResponse struct { + Success bool `json:"success"` // 注册是否成功 + Message string `json:"message"` // 返回信息 +} + +type RegisterServiceRequest struct { + Secret string `json:"secret"` // 通讯密钥 + ProxyPath string `json:"proxy_path"` // 代理路径 + ServiceURL string `json:"service_url"` // 服务地址 + Repository string `json:"repository"` // 服务代码仓库 + ServiceName string `json:"service_name"` // 服务名称 + ServiceVersion string `json:"service_version"` // 服务版本 +} diff --git a/internal/server.go b/internal/server.go index 64c2704..559c510 100644 --- a/internal/server.go +++ b/internal/server.go @@ -8,6 +8,7 @@ import ( "net/http" "time" + "github.com/perfect-panel/server/internal/report" "github.com/perfect-panel/server/pkg/logger" "github.com/perfect-panel/server/pkg/proc" @@ -65,9 +66,32 @@ func (m *Service) Start() { if m.svc == nil { panic("config file path is nil") } + // init service r := initServer(m.svc) - serverAddr := fmt.Sprintf("%v:%d", m.svc.Config.Host, m.svc.Config.Port) + // get server port + port := m.svc.Config.Port + host := m.svc.Config.Host + // check gateway mode + if report.IsGatewayMode() { + // get free port + freePort, err := report.ModulePort() + if err != nil { + logger.Errorf("get module port error: %s", err.Error()) + panic(err) + } + port = freePort + host = "127.0.0.1" + // register module + err = report.RegisterModule(port) + if err != nil { + logger.Errorf("register module error: %s", err.Error()) + panic(err) + } + logger.Infof("module registered on port %d", port) + } + + serverAddr := fmt.Sprintf("%v:%d", host, port) m.server = &http.Server{ Addr: serverAddr, Handler: r, From 2305562a7c0c8781524fa904b280a850c3b47858 Mon Sep 17 00:00:00 2001 From: Chang lue Tsen Date: Sun, 9 Nov 2025 08:46:53 -0500 Subject: [PATCH 05/50] feat(server): implement gateway mode handling and dynamic port registration --- initialize/config.go | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/initialize/config.go b/initialize/config.go index 025220f..0667543 100644 --- a/initialize/config.go +++ b/initialize/config.go @@ -9,6 +9,7 @@ import ( "net/http" "os" + "github.com/perfect-panel/server/internal/report" "github.com/perfect-panel/server/pkg/logger" "gorm.io/driver/mysql" @@ -35,10 +36,30 @@ func Config(path string) (chan bool, *http.Server) { configPath = path // Create a new Gin instance r := gin.Default() + // get server port + port := 8080 + host := "127.0.0.1" + // check gateway mode + if report.IsGatewayMode() { + // get free port + freePort, err := report.ModulePort() + if err != nil { + logger.Errorf("get module port error: %s", err.Error()) + panic(err) + } + port = freePort + // register module + err = report.RegisterModule(port) + if err != nil { + logger.Errorf("register module error: %s", err.Error()) + panic(err) + } + logger.Infof("module registered on port %d", port) + } // Create a new HTTP server server := &http.Server{ - Addr: ":8080", + Addr: fmt.Sprintf("%s:%d", host, port), Handler: r, } // Load templates From 87b743a2a2824ae416ed6e207fe004d8c739af81 Mon Sep 17 00:00:00 2001 From: Chang lue Tsen Date: Sun, 9 Nov 2025 08:54:50 -0500 Subject: [PATCH 06/50] refactor(constants): update repository and service name constants in tool and version files --- internal/report/tool.go | 4 ++-- pkg/constant/version.go | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/internal/report/tool.go b/internal/report/tool.go index 25438cc..54e87ac 100644 --- a/internal/report/tool.go +++ b/internal/report/tool.go @@ -75,8 +75,8 @@ func RegisterModule(port int) error { Secret: value, ProxyPath: "/api", ServiceURL: fmt.Sprintf("http://127.0.0.1:%d", port), - Repository: "https://github.com/perfect-panel/server", - ServiceName: "ApiService", + Repository: constant.Repository, + ServiceName: constant.ServiceName, ServiceVersion: constant.Version, }).SetResult(&response).Post(RegisterAPI) diff --git a/pkg/constant/version.go b/pkg/constant/version.go index 5370107..b9e58ed 100644 --- a/pkg/constant/version.go +++ b/pkg/constant/version.go @@ -2,6 +2,8 @@ package constant // Version PPanel version var ( - Version = "unknown version" - BuildTime = "unknown time" + Version = "unknown version" + BuildTime = "unknown time" + Repository = "https://github.com/perfect-panel/server" + ServiceName = "ApiService" ) From cb5bf5aae34e3ef15c1c3d9799c40f60bcce906c Mon Sep 17 00:00:00 2001 From: Chang lue Tsen Date: Sun, 9 Nov 2025 09:06:42 -0500 Subject: [PATCH 07/50] feat(module): add GetModuleConfig handler and logic for module configuration retrieval --- .../admin/system/getModuleConfigHandler.go | 18 ++++++++ internal/handler/routes.go | 6 +-- .../admin/system/getModuleConfigLogic.go | 41 +++++++++++++++++++ internal/types/types.go | 14 +++---- 4 files changed, 68 insertions(+), 11 deletions(-) create mode 100644 internal/handler/admin/system/getModuleConfigHandler.go create mode 100644 internal/logic/admin/system/getModuleConfigLogic.go diff --git a/internal/handler/admin/system/getModuleConfigHandler.go b/internal/handler/admin/system/getModuleConfigHandler.go new file mode 100644 index 0000000..72f87a3 --- /dev/null +++ b/internal/handler/admin/system/getModuleConfigHandler.go @@ -0,0 +1,18 @@ +package system + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/system" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" +) + +// GetModuleConfigHandler Get Module Config +func GetModuleConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := system.NewGetModuleConfigLogic(c.Request.Context(), svcCtx) + resp, err := l.GetModuleConfig() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/routes.go b/internal/handler/routes.go index 943ba84..03ea6c1 100644 --- a/internal/handler/routes.go +++ b/internal/handler/routes.go @@ -408,6 +408,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Update invite config adminSystemGroupRouter.PUT("/invite_config", adminSystem.UpdateInviteConfigHandler(serverCtx)) + // Get Module Config + adminSystemGroupRouter.GET("/module", adminSystem.GetModuleConfigHandler(serverCtx)) + // Get node config adminSystemGroupRouter.GET("/node_config", adminSystem.GetNodeConfigHandler(serverCtx)) @@ -822,9 +825,6 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Reset User Subscribe Token publicUserGroupRouter.PUT("/subscribe_token", publicUser.ResetUserSubscribeTokenHandler(serverCtx)) - // Update User Subscribe Note - publicUserGroupRouter.PUT("/subscribe_note", publicUser.UpdateUserSubscribeNoteHandler(serverCtx)) - // Unbind Device publicUserGroupRouter.PUT("/unbind_device", publicUser.UnbindDeviceHandler(serverCtx)) diff --git a/internal/logic/admin/system/getModuleConfigLogic.go b/internal/logic/admin/system/getModuleConfigLogic.go new file mode 100644 index 0000000..2dc646f --- /dev/null +++ b/internal/logic/admin/system/getModuleConfigLogic.go @@ -0,0 +1,41 @@ +package system + +import ( + "context" + "os" + + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetModuleConfigLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get Module Config +func NewGetModuleConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetModuleConfigLogic { + return &GetModuleConfigLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetModuleConfigLogic) GetModuleConfig() (resp *types.ModuleConfig, err error) { + value, exists := os.LookupEnv("SECRET_KEY") + if !exists { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), " SECRET_KEY not set in environment variables") + } + + return &types.ModuleConfig{ + Secret: value, + ServiceName: constant.ServiceName, + ServiceVersion: constant.Version, + }, nil +} diff --git a/internal/types/types.go b/internal/types/types.go index 9611bac..ddd090f 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -1221,6 +1221,12 @@ type MobileAuthenticateConfig struct { Whitelist []string `json:"whitelist"` } +type ModuleConfig struct { + Secret string `json:"secret"` // 通讯密钥 + ServiceName string `json:"service_name"` // 服务名称 + ServiceVersion string `json:"service_version"` // 服务版本 +} + type Node struct { Id int64 `json:"id"` Name string `json:"name"` @@ -1804,11 +1810,6 @@ type ResetUserSubscribeTokenRequest struct { UserSubscribeId int64 `json:"user_subscribe_id"` } -type UpdateUserSubscribeNoteRequest struct { - UserSubscribeId int64 `json:"user_subscribe_id" validate:"required"` - Note string `json:"note" validate:"max=500"` -} - type RevenueStatisticsResponse struct { Today OrdersStatistics `json:"today"` Monthly OrdersStatistics `json:"monthly"` @@ -2581,7 +2582,6 @@ type UserSubscribe struct { Upload int64 `json:"upload"` Token string `json:"token"` Status uint8 `json:"status"` - Note string `json:"note"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` } @@ -2601,7 +2601,6 @@ type UserSubscribeDetail struct { Upload int64 `json:"upload"` Token string `json:"token"` Status uint8 `json:"status"` - Note string `json:"note"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` } @@ -2620,7 +2619,6 @@ type UserSubscribeInfo struct { Upload int64 `json:"upload"` Token string `json:"token"` Status uint8 `json:"status"` - Note string `json:"note"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` IsTryOut bool `json:"is_try_out"` From d0aad68bb035bb49dd5af1cf631c8c2244cd03c0 Mon Sep 17 00:00:00 2001 From: Chang lue Tsen Date: Sun, 9 Nov 2025 09:07:11 -0500 Subject: [PATCH 08/50] style --- apis/admin/system.api | 13 ++++++- apis/auth/auth.api | 34 ++++++++--------- apis/common.api | 6 +-- apis/public/portal.api | 6 +-- apis/public/subscribe.api | 77 +++++++++++++++++++-------------------- apis/public/user.api | 28 +++++++------- apis/types.api | 17 ++++----- 7 files changed, 91 insertions(+), 90 deletions(-) diff --git a/apis/admin/system.api b/apis/admin/system.api index a300cda..d82d3bd 100644 --- a/apis/admin/system.api +++ b/apis/admin/system.api @@ -19,8 +19,13 @@ type ( Periods []TimePeriod `json:"periods"` } PreViewNodeMultiplierResponse { - CurrentTime string `json:"current_time"` - Ratio float32 `json:"ratio"` + CurrentTime string `json:"current_time"` + Ratio float32 `json:"ratio"` + } + ModuleConfig { + Secret string `json:"secret"` // 通讯密钥 + ServiceName string `json:"service_name"` // 服务名称 + ServiceVersion string `json:"service_version"` // 服务版本 } ) @@ -125,5 +130,9 @@ service ppanel { @doc "PreView Node Multiplier" @handler PreViewNodeMultiplier get /node_multiplier/preview returns (PreViewNodeMultiplierResponse) + + @doc "Get Module Config" + @handler GetModuleConfig + get /module returns (ModuleConfig) } diff --git a/apis/auth/auth.api b/apis/auth/auth.api index 154c878..8211bef 100644 --- a/apis/auth/auth.api +++ b/apis/auth/auth.api @@ -16,7 +16,7 @@ type ( Password string `json:"password" validate:"required"` IP string `header:"X-Original-Forwarded-For"` UserAgent string `header:"User-Agent"` - LoginType string `header:"Login-Type"` + LoginType string `header:"Login-Type"` CfToken string `json:"cf_token,optional"` } // Check user is exist request @@ -36,19 +36,19 @@ type ( Code string `json:"code,optional"` IP string `header:"X-Original-Forwarded-For"` UserAgent string `header:"User-Agent"` - LoginType string `header:"Login-Type"` + LoginType string `header:"Login-Type"` CfToken string `json:"cf_token,optional"` } // User login response ResetPasswordRequest { - Identifier string `json:"identifier"` - Email string `json:"email" validate:"required"` - Password string `json:"password" validate:"required"` - Code string `json:"code,optional"` - IP string `header:"X-Original-Forwarded-For"` - UserAgent string `header:"User-Agent"` - LoginType string `header:"Login-Type"` - CfToken string `json:"cf_token,optional"` + Identifier string `json:"identifier"` + Email string `json:"email" validate:"required"` + Password string `json:"password" validate:"required"` + Code string `json:"code,optional"` + IP string `header:"X-Original-Forwarded-For"` + UserAgent string `header:"User-Agent"` + LoginType string `header:"Login-Type"` + CfToken string `json:"cf_token,optional"` } LoginResponse { Token string `json:"token"` @@ -73,7 +73,7 @@ type ( Password string `json:"password"` IP string `header:"X-Original-Forwarded-For"` UserAgent string `header:"User-Agent"` - LoginType string `header:"Login-Type"` + LoginType string `header:"Login-Type"` CfToken string `json:"cf_token,optional"` } // Check user is exist request @@ -95,19 +95,19 @@ type ( Code string `json:"code,optional"` IP string `header:"X-Original-Forwarded-For"` UserAgent string `header:"User-Agent"` - LoginType string `header:"Login-Type,optional"` + LoginType string `header:"Login-Type,optional"` CfToken string `json:"cf_token,optional"` } // User login response TelephoneResetPasswordRequest { - Identifier string `json:"identifier"` + Identifier string `json:"identifier"` Telephone string `json:"telephone" validate:"required"` TelephoneAreaCode string `json:"telephone_area_code" validate:"required"` Password string `json:"password" validate:"required"` Code string `json:"code,optional"` IP string `header:"X-Original-Forwarded-For"` UserAgent string `header:"User-Agent"` - LoginType string `header:"Login-Type,optional"` + LoginType string `header:"Login-Type,optional"` CfToken string `json:"cf_token,optional"` } AppleLoginCallbackRequest { @@ -128,9 +128,9 @@ type ( ) @server ( - prefix: v1/auth - group: auth - middleware: DeviceMiddleware + prefix: v1/auth + group: auth + middleware: DeviceMiddleware ) service ppanel { @doc "User login" diff --git a/apis/common.api b/apis/common.api index d246099..e965d5e 100644 --- a/apis/common.api +++ b/apis/common.api @@ -90,9 +90,9 @@ type ( ) @server ( - prefix: v1/common - group: common - middleware: DeviceMiddleware + prefix: v1/common + group: common + middleware: DeviceMiddleware ) service ppanel { @doc "Get global config" diff --git a/apis/public/portal.api b/apis/public/portal.api index aba8e25..2d33861 100644 --- a/apis/public/portal.api +++ b/apis/public/portal.api @@ -68,9 +68,9 @@ type ( ) @server ( - prefix: v1/public/portal - group: public/portal - middleware: DeviceMiddleware + prefix: v1/public/portal + group: public/portal + middleware: DeviceMiddleware ) service ppanel { @doc "Get available payment methods" diff --git a/apis/public/subscribe.api b/apis/public/subscribe.api index 7ab01c2..25234ad 100644 --- a/apis/public/subscribe.api +++ b/apis/public/subscribe.api @@ -14,43 +14,40 @@ type ( QuerySubscribeListRequest { Language string `form:"language"` } - - QueryUserSubscribeNodeListResponse { - List []UserSubscribeInfo `json:"list"` - } - - UserSubscribeInfo { - Id int64 `json:"id"` - UserId int64 `json:"user_id"` - OrderId int64 `json:"order_id"` - SubscribeId int64 `json:"subscribe_id"` - StartTime int64 `json:"start_time"` - ExpireTime int64 `json:"expire_time"` - FinishedAt int64 `json:"finished_at"` - ResetTime int64 `json:"reset_time"` - Traffic int64 `json:"traffic"` - Download int64 `json:"download"` - Upload int64 `json:"upload"` - Token string `json:"token"` - Status uint8 `json:"status"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` - IsTryOut bool `json:"is_try_out"` - Nodes []*UserSubscribeNodeInfo `json:"nodes"` - } - - UserSubscribeNodeInfo{ - Id int64 `json:"id"` - Name string `json:"name"` - Uuid string `json:"uuid"` - Protocol string `json:"protocol"` - Port uint16 `json:"port"` - Address string `json:"address"` - Tags []string `json:"tags"` - Country string `json:"country"` - City string `json:"city"` - CreatedAt int64 `json:"created_at"` - } + QueryUserSubscribeNodeListResponse { + List []UserSubscribeInfo `json:"list"` + } + UserSubscribeInfo { + Id int64 `json:"id"` + UserId int64 `json:"user_id"` + OrderId int64 `json:"order_id"` + SubscribeId int64 `json:"subscribe_id"` + StartTime int64 `json:"start_time"` + ExpireTime int64 `json:"expire_time"` + FinishedAt int64 `json:"finished_at"` + ResetTime int64 `json:"reset_time"` + Traffic int64 `json:"traffic"` + Download int64 `json:"download"` + Upload int64 `json:"upload"` + Token string `json:"token"` + Status uint8 `json:"status"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + IsTryOut bool `json:"is_try_out"` + Nodes []*UserSubscribeNodeInfo `json:"nodes"` + } + UserSubscribeNodeInfo { + Id int64 `json:"id"` + Name string `json:"name"` + Uuid string `json:"uuid"` + Protocol string `json:"protocol"` + Port uint16 `json:"port"` + Address string `json:"address"` + Tags []string `json:"tags"` + Country string `json:"country"` + City string `json:"city"` + CreatedAt int64 `json:"created_at"` + } ) @server ( @@ -63,8 +60,8 @@ service ppanel { @handler QuerySubscribeList get /list (QuerySubscribeListRequest) returns (QuerySubscribeListResponse) - @doc "Get user subscribe node info" - @handler QueryUserSubscribeNodeList - get /node/list returns (QueryUserSubscribeNodeListResponse) + @doc "Get user subscribe node info" + @handler QueryUserSubscribeNodeList + get /node/list returns (QueryUserSubscribeNodeListResponse) } diff --git a/apis/public/user.api b/apis/public/user.api index 1686b32..dbd2160 100644 --- a/apis/public/user.api +++ b/apis/public/user.api @@ -97,15 +97,13 @@ type ( Email string `json:"email" validate:"required"` Code string `json:"code" validate:"required"` } - - GetDeviceListResponse { - List []UserDevice `json:"list"` - Total int64 `json:"total"` - } - - UnbindDeviceRequest { - Id int64 `json:"id" validate:"required"` - } + GetDeviceListResponse { + List []UserDevice `json:"list"` + Total int64 `json:"total"` + } + UnbindDeviceRequest { + Id int64 `json:"id" validate:"required"` + } ) @server ( @@ -202,12 +200,12 @@ service ppanel { @handler UpdateBindEmail put /bind_email (UpdateBindEmailRequest) - @doc "Get Device List" - @handler GetDeviceList - get /devices returns (GetDeviceListResponse) + @doc "Get Device List" + @handler GetDeviceList + get /devices returns (GetDeviceListResponse) - @doc "Unbind Device" - @handler UnbindDevice - put /unbind_device (UnbindDeviceRequest) + @doc "Unbind Device" + @handler UnbindDevice + put /unbind_device (UnbindDeviceRequest) } diff --git a/apis/types.api b/apis/types.api index c55a5df..3cecd4a 100644 --- a/apis/types.api +++ b/apis/types.api @@ -115,7 +115,7 @@ type ( AuthConfig { Mobile MobileAuthenticateConfig `json:"mobile"` Email EmailAuthticateConfig `json:"email"` - Device DeviceAuthticateConfig `json:"device"` + Device DeviceAuthticateConfig `json:"device"` Register PubilcRegisterConfig `json:"register"` } PubilcRegisterConfig { @@ -135,14 +135,12 @@ type ( EnableDomainSuffix bool `json:"enable_domain_suffix"` DomainSuffixList string `json:"domain_suffix_list"` } - - DeviceAuthticateConfig { - Enable bool `json:"enable"` - ShowAds bool `json:"show_ads"` - EnableSecurity bool `json:"enable_security"` - OnlyRealDevice bool `json:"only_real_device"` - } - + DeviceAuthticateConfig { + Enable bool `json:"enable"` + ShowAds bool `json:"show_ads"` + EnableSecurity bool `json:"enable_security"` + OnlyRealDevice bool `json:"only_real_device"` + } RegisterConfig { StopRegister bool `json:"stop_register"` EnableTrial bool `json:"enable_trial"` @@ -673,7 +671,6 @@ type ( List []SubscribeGroup `json:"list"` Total int64 `json:"total"` } - GetUserSubscribeTrafficLogsRequest { Page int `form:"page"` Size int `form:"size"` From 750a33cca288faaccf9cfe0cabd1a6c19a723cc6 Mon Sep 17 00:00:00 2001 From: Chang lue Tsen Date: Sun, 9 Nov 2025 09:14:09 -0500 Subject: [PATCH 09/50] feat(user): add UpdateUserSubscribeNote handler and endpoint for updating user subscription notes --- apis/public/user.api | 8 ++++++++ internal/handler/routes.go | 3 +++ internal/types/types.go | 5 +++++ 3 files changed, 16 insertions(+) diff --git a/apis/public/user.api b/apis/public/user.api index dbd2160..92d0c19 100644 --- a/apis/public/user.api +++ b/apis/public/user.api @@ -104,6 +104,10 @@ type ( UnbindDeviceRequest { Id int64 `json:"id" validate:"required"` } + UpdateUserSubscribeNoteRequest { + UserSubscribeId int64 `json:"user_subscribe_id" validate:"required"` + Note string `json:"note" validate:"max=500"` + } ) @server ( @@ -207,5 +211,9 @@ service ppanel { @doc "Unbind Device" @handler UnbindDevice put /unbind_device (UnbindDeviceRequest) + + @doc "Update User Subscribe Note" + @handler UpdateUserSubscribeNote + put /subscribe_note (UpdateUserSubscribeNoteRequest) } diff --git a/internal/handler/routes.go b/internal/handler/routes.go index 03ea6c1..282110d 100644 --- a/internal/handler/routes.go +++ b/internal/handler/routes.go @@ -822,6 +822,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Get Subscribe Log publicUserGroupRouter.GET("/subscribe_log", publicUser.GetSubscribeLogHandler(serverCtx)) + // Update User Subscribe Note + publicUserGroupRouter.PUT("/subscribe_note", publicUser.UpdateUserSubscribeNoteHandler(serverCtx)) + // Reset User Subscribe Token publicUserGroupRouter.PUT("/subscribe_token", publicUser.ResetUserSubscribeTokenHandler(serverCtx)) diff --git a/internal/types/types.go b/internal/types/types.go index ddd090f..79c3490 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -2459,6 +2459,11 @@ type UpdateUserPasswordRequest struct { Password string `json:"password" validate:"required"` } +type UpdateUserSubscribeNoteRequest struct { + UserSubscribeId int64 `json:"user_subscribe_id" validate:"required"` + Note string `json:"note" validate:"max=500"` +} + type UpdateUserSubscribeRequest struct { UserSubscribeId int64 `json:"user_subscribe_id"` SubscribeId int64 `json:"subscribe_id"` From 1172ecc2f191aeb24103115c4a32ba2d6ca6501f Mon Sep 17 00:00:00 2001 From: Chang lue Tsen Date: Sun, 9 Nov 2025 10:49:51 -0500 Subject: [PATCH 10/50] feat(heartbeat): add heartbeat endpoint and logic for service health check --- apis/common.api | 9 ++++++ internal/handler/common/heartbeatHandler.go | 18 +++++++++++ internal/handler/routes.go | 3 ++ internal/logic/common/heartbeatLogic.go | 33 +++++++++++++++++++++ internal/types/types.go | 6 ++++ 5 files changed, 69 insertions(+) create mode 100644 internal/handler/common/heartbeatHandler.go create mode 100644 internal/logic/common/heartbeatLogic.go diff --git a/apis/common.api b/apis/common.api index e965d5e..db935f4 100644 --- a/apis/common.api +++ b/apis/common.api @@ -87,6 +87,11 @@ type ( Total int64 `json:"total"` List []SubscribeClient `json:"list"` } + HeartbeatResponse { + Status bool `json:"status"` + Message string `json:"message,omitempty"` + Timestamp int64 `json:"timestamp,omitempty"` + } ) @server ( @@ -130,5 +135,9 @@ service ppanel { @doc "Get Client" @handler GetClient get /client returns (GetSubscribeClientResponse) + + @doc "Heartbeat" + @handler Heartbeat + get /heartbeat returns (HeartbeatResponse) } diff --git a/internal/handler/common/heartbeatHandler.go b/internal/handler/common/heartbeatHandler.go new file mode 100644 index 0000000..d72d771 --- /dev/null +++ b/internal/handler/common/heartbeatHandler.go @@ -0,0 +1,18 @@ +package common + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/common" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" +) + +// Heartbeat +func HeartbeatHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := common.NewHeartbeatLogic(c.Request.Context(), svcCtx) + resp, err := l.Heartbeat() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/routes.go b/internal/handler/routes.go index 282110d..5e0364b 100644 --- a/internal/handler/routes.go +++ b/internal/handler/routes.go @@ -638,6 +638,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Get Client commonGroupRouter.GET("/client", common.GetClientHandler(serverCtx)) + // Heartbeat + commonGroupRouter.GET("/heartbeat", common.HeartbeatHandler(serverCtx)) + // Get verification code commonGroupRouter.POST("/send_code", common.SendEmailCodeHandler(serverCtx)) diff --git a/internal/logic/common/heartbeatLogic.go b/internal/logic/common/heartbeatLogic.go new file mode 100644 index 0000000..1bfb081 --- /dev/null +++ b/internal/logic/common/heartbeatLogic.go @@ -0,0 +1,33 @@ +package common + +import ( + "context" + "time" + + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" +) + +type HeartbeatLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewHeartbeatLogic Heartbeat +func NewHeartbeatLogic(ctx context.Context, svcCtx *svc.ServiceContext) *HeartbeatLogic { + return &HeartbeatLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *HeartbeatLogic) Heartbeat() (resp *types.HeartbeatResponse, err error) { + return &types.HeartbeatResponse{ + Status: true, + Message: "service is alive", + Timestamp: time.Now().Unix(), + }, nil +} diff --git a/internal/types/types.go b/internal/types/types.go index 79c3490..b8b39ab 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -1158,6 +1158,12 @@ type HasMigrateSeverNodeResponse struct { HasMigrate bool `json:"has_migrate"` } +type HeartbeatResponse struct { + Status bool `json:"status"` + Message string `json:"message,omitempty"` + Timestamp int64 `json:"timestamp,omitempty"` +} + type Hysteria2 struct { Port int `json:"port" validate:"required"` HopPorts string `json:"hop_ports" validate:"required"` From c8b3683cf24aabf62e5db392fffdd479038ae80a Mon Sep 17 00:00:00 2001 From: Chang lue Tsen Date: Sun, 9 Nov 2025 10:56:55 -0500 Subject: [PATCH 11/50] feat(report): add heartbeat URL to registration request and response --- internal/report/report.go | 13 +------------ internal/report/tool.go | 1 + internal/report/types.go | 1 + 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/internal/report/report.go b/internal/report/report.go index 31768e4..8b7faa9 100644 --- a/internal/report/report.go +++ b/internal/report/report.go @@ -1,20 +1,9 @@ package report const ( - GatewayURL = "http://127.0.0.1:%d" // 网关地址 - RegisterAPI = "/basic/register" // 模块注册接口 + RegisterAPI = "/basic/register" // 模块注册接口 ) -// RegisterRequest 模块注册请求参数 -type RegisterRequest struct { - Secret string `json:"secret"` // 通讯密钥 - ProxyPath string `json:"proxy_path"` // 代理路径 - ServiceURL string `json:"service_url"` // 服务地址 - Repository string `json:"repository"` // 服务代码仓库 - ServiceName string `json:"service_name"` // 服务名称 - ServiceVersion string `json:"service_version"` // 服务版本 -} - // RegisterResponse 模块注册响应参数 type RegisterResponse struct { Success bool `json:"success"` // 注册是否成功 diff --git a/internal/report/tool.go b/internal/report/tool.go index 54e87ac..cd57f1e 100644 --- a/internal/report/tool.go +++ b/internal/report/tool.go @@ -76,6 +76,7 @@ func RegisterModule(port int) error { ProxyPath: "/api", ServiceURL: fmt.Sprintf("http://127.0.0.1:%d", port), Repository: constant.Repository, + HeartbeatURL: fmt.Sprintf("http://127.0.0.1:%d/v1/common/heartbeat", port), ServiceName: constant.ServiceName, ServiceVersion: constant.Version, }).SetResult(&response).Post(RegisterAPI) diff --git a/internal/report/types.go b/internal/report/types.go index 51d3c1a..6abf9b2 100644 --- a/internal/report/types.go +++ b/internal/report/types.go @@ -12,5 +12,6 @@ type RegisterServiceRequest struct { ServiceURL string `json:"service_url"` // 服务地址 Repository string `json:"repository"` // 服务代码仓库 ServiceName string `json:"service_name"` // 服务名称 + HeartbeatURL string `json:"heartbeat_url"` // 心跳检测地址 ServiceVersion string `json:"service_version"` // 服务版本 } From 8a4cfcbdb38c48fc2526f05b12f8ed3ab0ceca4e Mon Sep 17 00:00:00 2001 From: Chang lue Tsen Date: Tue, 18 Nov 2025 08:37:57 -0500 Subject: [PATCH 12/50] feat(subscribe): add endpoint to reset all subscribe tokens --- apis/admin/subscribe.api | 7 +++ .../resetAllSubscribeTokenHandler.go | 18 ++++++ internal/handler/routes.go | 3 + .../subscribe/resetAllSubscribeTokenLogic.go | 61 +++++++++++++++++++ internal/types/types.go | 4 ++ 5 files changed, 93 insertions(+) create mode 100644 internal/handler/admin/subscribe/resetAllSubscribeTokenHandler.go create mode 100644 internal/logic/admin/subscribe/resetAllSubscribeTokenLogic.go diff --git a/apis/admin/subscribe.api b/apis/admin/subscribe.api index 1d08b65..bea205d 100644 --- a/apis/admin/subscribe.api +++ b/apis/admin/subscribe.api @@ -102,6 +102,9 @@ type ( BatchDeleteSubscribeRequest { Ids []int64 `json:"ids" validate:"required"` } + ResetAllSubscribeTokenResponse { + Success bool `json:"success"` + } ) @server ( @@ -157,5 +160,9 @@ service ppanel { @doc "Subscribe sort" @handler SubscribeSort post /sort (SubscribeSortRequest) + + @doc "Reset all subscribe tokens" + @handler ResetAllSubscribeToken + post /reset_all_token returns (ResetAllSubscribeTokenResponse) } diff --git a/internal/handler/admin/subscribe/resetAllSubscribeTokenHandler.go b/internal/handler/admin/subscribe/resetAllSubscribeTokenHandler.go new file mode 100644 index 0000000..408975a --- /dev/null +++ b/internal/handler/admin/subscribe/resetAllSubscribeTokenHandler.go @@ -0,0 +1,18 @@ +package subscribe + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/subscribe" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" +) + +// Reset all subscribe tokens +func ResetAllSubscribeTokenHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := subscribe.NewResetAllSubscribeTokenLogic(c.Request.Context(), svcCtx) + resp, err := l.ResetAllSubscribeToken() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/routes.go b/internal/handler/routes.go index 5e0364b..542d66c 100644 --- a/internal/handler/routes.go +++ b/internal/handler/routes.go @@ -385,6 +385,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Get subscribe list adminSubscribeGroupRouter.GET("/list", adminSubscribe.GetSubscribeListHandler(serverCtx)) + // Reset all subscribe tokens + adminSubscribeGroupRouter.POST("/reset_all_token", adminSubscribe.ResetAllSubscribeTokenHandler(serverCtx)) + // Subscribe sort adminSubscribeGroupRouter.POST("/sort", adminSubscribe.SubscribeSortHandler(serverCtx)) } diff --git a/internal/logic/admin/subscribe/resetAllSubscribeTokenLogic.go b/internal/logic/admin/subscribe/resetAllSubscribeTokenLogic.go new file mode 100644 index 0000000..e7307a2 --- /dev/null +++ b/internal/logic/admin/subscribe/resetAllSubscribeTokenLogic.go @@ -0,0 +1,61 @@ +package subscribe + +import ( + "context" + "strconv" + "time" + + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/pkg/uuidx" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" + + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" +) + +type ResetAllSubscribeTokenLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Reset all subscribe tokens +func NewResetAllSubscribeTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ResetAllSubscribeTokenLogic { + return &ResetAllSubscribeTokenLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *ResetAllSubscribeTokenLogic) ResetAllSubscribeToken() (resp *types.ResetAllSubscribeTokenResponse, err error) { + var list []*user.Subscribe + tx := l.svcCtx.DB.WithContext(l.ctx).Begin() + // select all active and Finished subscriptions + if err = tx.Model(&user.Subscribe{}).Where("`status` IN ?", []int64{1, 2}).Find(&list).Error; err != nil { + logger.Errorf("[ResetAllSubscribeToken] Failed to fetch subscribe list: %v", err.Error()) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Failed to fetch subscribe list: %v", err.Error()) + } + + for _, sub := range list { + sub.Token = uuidx.SubscribeToken(strconv.FormatInt(time.Now().UnixMilli(), 10) + strconv.FormatInt(sub.Id, 10)) + sub.UUID = uuidx.NewUUID().String() + if err = tx.Model(&user.Subscribe{}).Where("id = ?", sub.Id).Save(sub).Error; err != nil { + tx.Rollback() + logger.Errorf("[ResetAllSubscribeToken] Failed to update subscribe token for ID %d: %v", sub.Id, err.Error()) + return &types.ResetAllSubscribeTokenResponse{ + Success: false, + }, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "Failed to update subscribe token for ID %d: %v", sub.Id, err.Error()) + } + } + if err = tx.Commit().Error; err != nil { + logger.Errorf("[ResetAllSubscribeToken] Failed to commit transaction: %v", err.Error()) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "Failed to commit transaction: %v", err.Error()) + } + + return &types.ResetAllSubscribeTokenResponse{ + Success: true, + }, nil +} diff --git a/internal/types/types.go b/internal/types/types.go index b8b39ab..20a2b0f 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -1772,6 +1772,10 @@ type RenewalOrderResponse struct { OrderNo string `json:"order_no"` } +type ResetAllSubscribeTokenResponse struct { + Success bool `json:"success"` +} + type ResetPasswordRequest struct { Identifier string `json:"identifier"` Email string `json:"email" validate:"required"` From e3999ba75f999fb1ef76b38fb3a2c3db9c04f1d3 Mon Sep 17 00:00:00 2001 From: Chang lue Tsen Date: Tue, 18 Nov 2025 12:03:14 -0500 Subject: [PATCH 13/50] feat(subscribe): add short token generation and validation logic --- apis/types.api | 1 + internal/handler/subscribe.go | 16 ++++++++ .../public/user/queryUserSubscribeLogic.go | 2 + internal/server.go | 2 +- internal/types/types.go | 1 + pkg/tool/string.go | 38 +++++++++++++++++++ pkg/tool/string_test.go | 27 +++++++++++++ 7 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 pkg/tool/string.go create mode 100644 pkg/tool/string_test.go diff --git a/apis/types.api b/apis/types.api index 3cecd4a..3f50d02 100644 --- a/apis/types.api +++ b/apis/types.api @@ -470,6 +470,7 @@ type ( Upload int64 `json:"upload"` Token string `json:"token"` Status uint8 `json:"status"` + Short string `json:"short"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` } diff --git a/internal/handler/subscribe.go b/internal/handler/subscribe.go index bf72a19..6c228ed 100644 --- a/internal/handler/subscribe.go +++ b/internal/handler/subscribe.go @@ -23,6 +23,22 @@ func SubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { ua := c.GetHeader("User-Agent") req.UA = c.Request.Header.Get("User-Agent") req.Flag = c.Query("flag") + if svcCtx.Config.Subscribe.PanDomain { + domain := c.Request.Host + domainArr := strings.Split(domain, ".") + short, err := tool.FixedUniqueString(req.Token, 8, "") + if err != nil { + logger.Errorf("[SubscribeHandler] Generate short token failed: %v", err) + c.String(http.StatusInternalServerError, "Internal Server") + c.Abort() + return + } + if short != domainArr[0] { + c.String(http.StatusForbidden, "Access denied") + c.Abort() + return + } + } if svcCtx.Config.Subscribe.UserAgentLimit { if ua == "" { diff --git a/internal/logic/public/user/queryUserSubscribeLogic.go b/internal/logic/public/user/queryUserSubscribeLogic.go index 757f6fc..55e3770 100644 --- a/internal/logic/public/user/queryUserSubscribeLogic.go +++ b/internal/logic/public/user/queryUserSubscribeLogic.go @@ -60,6 +60,8 @@ func (l *QueryUserSubscribeLogic) QueryUserSubscribe() (resp *types.QueryUserSub } } + short, _ := tool.FixedUniqueString(item.Token, 8, "") + sub.Short = short sub.ResetTime = calculateNextResetTime(&sub) resp.List = append(resp.List, sub) } diff --git a/internal/server.go b/internal/server.go index 559c510..47969be 100644 --- a/internal/server.go +++ b/internal/server.go @@ -49,7 +49,7 @@ func initServer(svc *svc.ServiceContext) *gin.Engine { } r.Use(sessions.Sessions("ppanel", sessionStore)) // use cors middleware - r.Use(middleware.TraceMiddleware(svc), middleware.LoggerMiddleware(svc), middleware.CorsMiddleware, middleware.PanDomainMiddleware(svc), gin.Recovery()) + r.Use(middleware.TraceMiddleware(svc), middleware.LoggerMiddleware(svc), middleware.CorsMiddleware, gin.Recovery()) // register handlers handler.RegisterHandlers(r, svc) diff --git a/internal/types/types.go b/internal/types/types.go index 20a2b0f..dfe28bd 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -2597,6 +2597,7 @@ type UserSubscribe struct { Upload int64 `json:"upload"` Token string `json:"token"` Status uint8 `json:"status"` + Short string `json:"short"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` } diff --git a/pkg/tool/string.go b/pkg/tool/string.go new file mode 100644 index 0000000..2567dc0 --- /dev/null +++ b/pkg/tool/string.go @@ -0,0 +1,38 @@ +package tool + +import ( + "crypto/sha256" + "encoding/binary" + "errors" + "math/rand" +) + +func FixedUniqueString(s string, length int, alphabet string) (string, error) { + if alphabet == "" { + alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + } + if length <= 0 { + return "", errors.New("length must be > 0") + } + if length > len(alphabet) { + return "", errors.New("length greater than available unique characters") + } + + // Generate deterministic seed from SHA256 + hash := sha256.Sum256([]byte(s)) + seed := int64(binary.LittleEndian.Uint64(hash[:8])) // 前 8 字节 + + r := rand.New(rand.NewSource(seed)) + + // Copy alphabet to mutable array + data := []rune(alphabet) + + // Deterministic shuffle (Fisher–Yates) + for i := len(data) - 1; i > 0; i-- { + j := r.Intn(i + 1) + data[i], data[j] = data[j], data[i] + } + + // Take first N characters + return string(data[:length]), nil +} diff --git a/pkg/tool/string_test.go b/pkg/tool/string_test.go new file mode 100644 index 0000000..0c44086 --- /dev/null +++ b/pkg/tool/string_test.go @@ -0,0 +1,27 @@ +package tool + +import ( + "testing" +) + +func TestFixedUniqueString(t *testing.T) { + a := "example" + b := "example1" + c := "example" + + strA1, err := FixedUniqueString(a, 8, "") + strB1, err := FixedUniqueString(b, 8, "") + strC1, err := FixedUniqueString(c, 8, "") + if err != nil { + t.Logf("Error: %v", err.Error()) + return + } + if strA1 != strC1 { + t.Errorf("Expected strA1 and strC1 to be equal, got %s and %s", strA1, strC1) + } + if strA1 == strB1 { + t.Errorf("Expected strA1 and strB1 to be different, got %s and %s", strA1, strB1) + } + t.Logf("strA1 and strB1 are not equal, strA1: %s, strB1: %s", strA1, strB1) + t.Logf("strA1 and strC1 are equal,strA1: %s, strC1: %s", strA1, strC1) +} From c4166cef6b3cf67c4b360900456c9af1252a15fc Mon Sep 17 00:00:00 2001 From: Tension Date: Sun, 23 Nov 2025 22:38:55 +0800 Subject: [PATCH 14/50] feat(ip-location): implement IP location querying and GeoIP database management --- apis/admin/tool.api | 12 +++ etc/ppanel.yaml | 39 ++++++++++ go.mod | 2 + go.sum | 4 + .../admin/tool/queryIPLocationHandler.go | 26 +++++++ internal/handler/common/getClientHandler.go | 1 - .../logic/admin/tool/queryIPLocationLogic.go | 57 ++++++++++++++ internal/svc/mmdb.go | 74 +++++++++++++++++++ internal/svc/serviceContext.go | 10 +++ internal/types/types.go | 13 ++++ 10 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 internal/handler/admin/tool/queryIPLocationHandler.go create mode 100644 internal/logic/admin/tool/queryIPLocationLogic.go create mode 100644 internal/svc/mmdb.go diff --git a/apis/admin/tool.api b/apis/admin/tool.api index da1916b..4d1f17b 100644 --- a/apis/admin/tool.api +++ b/apis/admin/tool.api @@ -17,6 +17,14 @@ type ( VersionResponse { Version string `json:"version"` } + QueryIPLocationRequest { + IP string `form:"ip" validate:"required"` + } + QueryIPLocationResponse { + Country string `json:"country"` + Region string `json:"region,omitempty"` + City string `json:"city"` + } ) @server ( @@ -36,5 +44,9 @@ service ppanel { @doc "Get Version" @handler GetVersion get /version returns (VersionResponse) + + @doc "Query IP Location" + @handler QueryIPLocation + get /ip/location (QueryIPLocationRequest) returns (QueryIPLocationResponse) } diff --git a/etc/ppanel.yaml b/etc/ppanel.yaml index e69de29..daba947 100644 --- a/etc/ppanel.yaml +++ b/etc/ppanel.yaml @@ -0,0 +1,39 @@ +Host: 0.0.0.0 +Port: 8080 +TLS: + Enable: false + CertFile: "" + KeyFile: "" +Debug: false +JwtAuth: + AccessSecret: 4672dccd-2c50-4833-8e17-81e6c2119893 + AccessExpire: 604800 +Logger: + ServiceName: PPanel + Mode: console + Encoding: plain + TimeFormat: "2006-01-02 15:04:05.000" + Path: logs + Level: info + MaxContentLength: 0 + Compress: false + Stat: true + KeepDays: 0 + StackCooldownMillis: 100 + MaxBackups: 0 + MaxSize: 0 + Rotation: daily + FileTimeFormat: 2006-01-02T15:04:05.000Z07:00 +MySQL: + Addr: localhost:3306 + Username: root + Password: password + Dbname: ppanel_dev + Config: charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai + MaxIdleConns: 10 + MaxOpenConns: 10 + SlowThreshold: 1000 +Redis: + Host: 127.0.0.1:6379 + Pass: "" + DB: 0 diff --git a/go.mod b/go.mod index 3e110be..9d0b191 100644 --- a/go.mod +++ b/go.mod @@ -60,6 +60,7 @@ require ( github.com/fatih/color v1.18.0 github.com/goccy/go-json v0.10.4 github.com/golang-migrate/migrate/v4 v4.18.2 + github.com/oschwald/geoip2-golang v1.13.0 github.com/spaolacci/murmur3 v1.1.0 google.golang.org/grpc v1.64.1 google.golang.org/protobuf v1.36.3 @@ -117,6 +118,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/openzipkin/zipkin-go v0.4.2 // indirect + github.com/oschwald/maxminddb-golang v1.13.0 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 6ab9d74..aa84f0e 100644 --- a/go.sum +++ b/go.sum @@ -285,6 +285,10 @@ github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQ github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/openzipkin/zipkin-go v0.4.2 h1:zjqfqHjUpPmB3c1GlCvvgsM1G4LkvqQbBDueDOCg/jA= github.com/openzipkin/zipkin-go v0.4.2/go.mod h1:ZeVkFjuuBiSy13y8vpSDCjMi9GoI3hPpCJSBx/EYFhY= +github.com/oschwald/geoip2-golang v1.13.0 h1:Q44/Ldc703pasJeP5V9+aFSZFmBN7DKHbNsSFzQATJI= +github.com/oschwald/geoip2-golang v1.13.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo= +github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU= +github.com/oschwald/maxminddb-golang v1.13.0/go.mod h1:BU0z8BfFVhi1LQaonTwwGQlsHUEu9pWNdMfmq4ztm0o= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= diff --git a/internal/handler/admin/tool/queryIPLocationHandler.go b/internal/handler/admin/tool/queryIPLocationHandler.go new file mode 100644 index 0000000..0b95355 --- /dev/null +++ b/internal/handler/admin/tool/queryIPLocationHandler.go @@ -0,0 +1,26 @@ +package tool + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/tool" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// QueryIPLocationHandler Query IP Location +func QueryIPLocationHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.QueryIPLocationRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := tool.NewQueryIPLocationLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryIPLocation(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/common/getClientHandler.go b/internal/handler/common/getClientHandler.go index e40b555..78d613d 100644 --- a/internal/handler/common/getClientHandler.go +++ b/internal/handler/common/getClientHandler.go @@ -10,7 +10,6 @@ import ( // Get Client func GetClientHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - l := common.NewGetClientLogic(c.Request.Context(), svcCtx) resp, err := l.GetClient() result.HttpResult(c, resp, err) diff --git a/internal/logic/admin/tool/queryIPLocationLogic.go b/internal/logic/admin/tool/queryIPLocationLogic.go new file mode 100644 index 0000000..6487b05 --- /dev/null +++ b/internal/logic/admin/tool/queryIPLocationLogic.go @@ -0,0 +1,57 @@ +package tool + +import ( + "context" + "net" + + "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 QueryIPLocationLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewQueryIPLocationLogic Query IP Location +func NewQueryIPLocationLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryIPLocationLogic { + return &QueryIPLocationLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryIPLocationLogic) QueryIPLocation(req *types.QueryIPLocationRequest) (resp *types.QueryIPLocationResponse, err error) { + if l.svcCtx.GeoIP == nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), " GeoIP database not configured") + } + + ip := net.ParseIP(req.IP) + record, err := l.svcCtx.GeoIP.DB.City(ip) + if err != nil { + l.Errorf("Failed to query IP location: %v", err) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Failed to query IP location") + } + + var country, region, city string + if record.Country.Names != nil { + country = record.Country.Names["en"] + } + if len(record.Subdivisions) > 0 && record.Subdivisions[0].Names != nil { + region = record.Subdivisions[0].Names["en"] + } + if record.City.Names != nil { + city = record.City.Names["en"] + } + + return &types.QueryIPLocationResponse{ + Country: country, + Region: region, + City: city, + }, nil +} diff --git a/internal/svc/mmdb.go b/internal/svc/mmdb.go new file mode 100644 index 0000000..331034f --- /dev/null +++ b/internal/svc/mmdb.go @@ -0,0 +1,74 @@ +package svc + +import ( + "io" + "net/http" + "os" + "path/filepath" + + "github.com/oschwald/geoip2-golang" + "github.com/perfect-panel/server/pkg/logger" +) + +const GeoIPDBURL = "https://raw.githubusercontent.com/adysec/IP_database/main/geolite/GeoLite2-City.mmdb" + +type IPLocation struct { + Path string + DB *geoip2.Reader +} + +func NewIPLocation(path string) (*IPLocation, error) { + + // 检查文件是否存在 + if _, err := os.Stat(path); os.IsNotExist(err) { + logger.Infof("[GeoIP] Database not found, downloading from %s", GeoIPDBURL) + // 文件不存在,下载数据库 + err := DownloadGeoIPDatabase(GeoIPDBURL, path) + if err != nil { + logger.Errorf("[GeoIP] Failed to download database: %v", err.Error()) + return nil, err + } + logger.Infof("[GeoIP] Database downloaded successfully") + } + + db, err := geoip2.Open(path) + if err != nil { + return nil, err + } + return &IPLocation{ + Path: path, + DB: db, + }, nil +} + +func (ipLoc *IPLocation) Close() error { + return ipLoc.DB.Close() +} + +func DownloadGeoIPDatabase(url, path string) error { + + // 创建路径, 确保目录存在 + err := os.MkdirAll(filepath.Dir(path), 0755) + if err != nil { + logger.Errorf("[GeoIP] Failed to create directory: %v", err.Error()) + return err + } + + // 创建文件 + out, err := os.Create(path) + if err != nil { + return err + } + defer out.Close() + + // 请求远程文件 + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + // 保存文件 + _, err = io.Copy(out, resp.Body) + return err +} diff --git a/internal/svc/serviceContext.go b/internal/svc/serviceContext.go index be05079..4f6bc3a 100644 --- a/internal/svc/serviceContext.go +++ b/internal/svc/serviceContext.go @@ -37,6 +37,7 @@ type ServiceContext struct { Config config.Config Queue *asynq.Client ExchangeRate float64 + GeoIP *IPLocation //NodeCache *cache.NodeCacheClient AuthModel auth.Model @@ -68,9 +69,17 @@ func NewServiceContext(c config.Config) *ServiceContext { db, err := orm.ConnectMysql(orm.Mysql{ Config: c.MySQL, }) + if err != nil { panic(err.Error()) } + + // IP location initialize + geoIP, err := NewIPLocation("./cache/GeoLite2-City.mmdb") + if err != nil { + panic(err.Error()) + } + rds := redis.NewClient(&redis.Options{ Addr: c.Redis.Host, Password: c.Redis.Pass, @@ -89,6 +98,7 @@ func NewServiceContext(c config.Config) *ServiceContext { Config: c, Queue: NewAsynqClient(c), ExchangeRate: 1.0, + GeoIP: geoIP, //NodeCache: cache.NewNodeCacheClient(rds), AuthLimiter: authLimiter, AdsModel: ads.NewModel(db, rds), diff --git a/internal/types/types.go b/internal/types/types.go index dfe28bd..1c54b3f 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -1571,6 +1571,16 @@ type QueryDocumentListResponse struct { List []Document `json:"list"` } +type QueryIPLocationRequest struct { + IP string `form:"ip" validate:"required"` +} + +type QueryIPLocationResponse struct { + Country string `json:"country"` + Region string `json:"regio,omitempty"` + City string `json:"city"` +} + type QueryNodeTagResponse struct { Tags []string `json:"tags"` } @@ -2598,6 +2608,7 @@ type UserSubscribe struct { Token string `json:"token"` Status uint8 `json:"status"` Short string `json:"short"` + Note string `json:"note"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` } @@ -2617,6 +2628,7 @@ type UserSubscribeDetail struct { Upload int64 `json:"upload"` Token string `json:"token"` Status uint8 `json:"status"` + Note string `json:"note"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` } @@ -2635,6 +2647,7 @@ type UserSubscribeInfo struct { Upload int64 `json:"upload"` Token string `json:"token"` Status uint8 `json:"status"` + Note string `json:"note"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` IsTryOut bool `json:"is_try_out"` From b29e5c8cb55d3b1059a1e1b20ccf0696e3fa5894 Mon Sep 17 00:00:00 2001 From: Tension Date: Mon, 24 Nov 2025 16:43:23 +0800 Subject: [PATCH 15/50] fix(ppanel): remove deprecated configuration settings from ppanel.yaml --- etc/ppanel.yaml | 39 --------------------------------------- 1 file changed, 39 deletions(-) diff --git a/etc/ppanel.yaml b/etc/ppanel.yaml index daba947..e69de29 100644 --- a/etc/ppanel.yaml +++ b/etc/ppanel.yaml @@ -1,39 +0,0 @@ -Host: 0.0.0.0 -Port: 8080 -TLS: - Enable: false - CertFile: "" - KeyFile: "" -Debug: false -JwtAuth: - AccessSecret: 4672dccd-2c50-4833-8e17-81e6c2119893 - AccessExpire: 604800 -Logger: - ServiceName: PPanel - Mode: console - Encoding: plain - TimeFormat: "2006-01-02 15:04:05.000" - Path: logs - Level: info - MaxContentLength: 0 - Compress: false - Stat: true - KeepDays: 0 - StackCooldownMillis: 100 - MaxBackups: 0 - MaxSize: 0 - Rotation: daily - FileTimeFormat: 2006-01-02T15:04:05.000Z07:00 -MySQL: - Addr: localhost:3306 - Username: root - Password: password - Dbname: ppanel_dev - Config: charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai - MaxIdleConns: 10 - MaxOpenConns: 10 - SlowThreshold: 1000 -Redis: - Host: 127.0.0.1:6379 - Pass: "" - DB: 0 From 429e535dd431cac281c19da8e4092ae127a2e173 Mon Sep 17 00:00:00 2001 From: Chang lue Tsen Date: Mon, 24 Nov 2025 09:05:49 -0500 Subject: [PATCH 16/50] feat(user): add endpoint and logic for updating user rules --- apis/public/user.api | 7 +++ apis/types.api | 1 + .../database/02120_user_rules.down.sql | 2 + .../migrate/database/02120_user_rules.up.sql | 4 ++ .../public/user/updateUserRulesHandler.go | 26 ++++++++++ internal/handler/routes.go | 6 +++ .../logic/public/user/updateUserRulesLogic.go | 51 +++++++++++++++++++ internal/model/user/user.go | 1 + internal/types/types.go | 10 ++-- 9 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 initialize/migrate/database/02120_user_rules.down.sql create mode 100644 initialize/migrate/database/02120_user_rules.up.sql create mode 100644 internal/handler/public/user/updateUserRulesHandler.go create mode 100644 internal/logic/public/user/updateUserRulesLogic.go diff --git a/apis/public/user.api b/apis/public/user.api index 92d0c19..45ebdef 100644 --- a/apis/public/user.api +++ b/apis/public/user.api @@ -108,6 +108,9 @@ type ( UserSubscribeId int64 `json:"user_subscribe_id" validate:"required"` Note string `json:"note" validate:"max=500"` } + UpdateUserRulesRequest { + Rules []string `json:"rules" validate:"required"` + } ) @server ( @@ -215,5 +218,9 @@ service ppanel { @doc "Update User Subscribe Note" @handler UpdateUserSubscribeNote put /subscribe_note (UpdateUserSubscribeNoteRequest) + + @doc "Update User Rules" + @handler UpdateUserRules + put /rules (UpdateUserRulesRequest) } diff --git a/apis/types.api b/apis/types.api index 3f50d02..0efa991 100644 --- a/apis/types.api +++ b/apis/types.api @@ -28,6 +28,7 @@ type ( EnableTradeNotify bool `json:"enable_trade_notify"` AuthMethods []UserAuthMethod `json:"auth_methods"` UserDevices []UserDevice `json:"user_devices"` + Rules []string `json:"rules"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` DeletedAt int64 `json:"deleted_at,omitempty"` diff --git a/initialize/migrate/database/02120_user_rules.down.sql b/initialize/migrate/database/02120_user_rules.down.sql new file mode 100644 index 0000000..718f4a6 --- /dev/null +++ b/initialize/migrate/database/02120_user_rules.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE `user` +DROP COLUMN IF EXISTS `rules`; diff --git a/initialize/migrate/database/02120_user_rules.up.sql b/initialize/migrate/database/02120_user_rules.up.sql new file mode 100644 index 0000000..5e93aca --- /dev/null +++ b/initialize/migrate/database/02120_user_rules.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE `user` + ADD COLUMN `rules` TEXT NULL + COMMENT 'User rules for subscription' + AFTER `created_at`; diff --git a/internal/handler/public/user/updateUserRulesHandler.go b/internal/handler/public/user/updateUserRulesHandler.go new file mode 100644 index 0000000..e8b9a01 --- /dev/null +++ b/internal/handler/public/user/updateUserRulesHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/public/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Update User Rules +func UpdateUserRulesHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UpdateUserRulesRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewUpdateUserRulesLogic(c.Request.Context(), svcCtx) + err := l.UpdateUserRules(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/routes.go b/internal/handler/routes.go index 542d66c..7faf037 100644 --- a/internal/handler/routes.go +++ b/internal/handler/routes.go @@ -493,6 +493,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { adminToolGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) { + // Query IP Location + adminToolGroupRouter.GET("/ip/location", adminTool.QueryIPLocationHandler(serverCtx)) + // Get System Log adminToolGroupRouter.GET("/log", adminTool.GetSystemLogHandler(serverCtx)) @@ -822,6 +825,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Update User Password publicUserGroupRouter.PUT("/password", publicUser.UpdateUserPasswordHandler(serverCtx)) + // Update User Rules + publicUserGroupRouter.PUT("/rules", publicUser.UpdateUserRulesHandler(serverCtx)) + // Query User Subscribe publicUserGroupRouter.GET("/subscribe", publicUser.QueryUserSubscribeHandler(serverCtx)) diff --git a/internal/logic/public/user/updateUserRulesLogic.go b/internal/logic/public/user/updateUserRulesLogic.go new file mode 100644 index 0000000..63ab169 --- /dev/null +++ b/internal/logic/public/user/updateUserRulesLogic.go @@ -0,0 +1,51 @@ +package user + +import ( + "context" + "encoding/json" + + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type UpdateUserRulesLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewUpdateUserRulesLogic Update User Rules +func NewUpdateUserRulesLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateUserRulesLogic { + return &UpdateUserRulesLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateUserRulesLogic) UpdateUserRules(req *types.UpdateUserRulesRequest) error { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + if len(req.Rules) > 0 { + bytes, err := json.Marshal(req.Rules) + if err != nil { + l.Logger.Errorf("UpdateUserRulesLogic json marshal rules error: %v", err) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "json marshal rules failed: %v", err.Error()) + } + u.Rules = string(bytes) + err = l.svcCtx.UserModel.Update(l.ctx, u) + if err != nil { + l.Logger.Errorf("UpdateUserRulesLogic UpdateUserRules error: %v", err) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update user rules failed: %v", err.Error()) + } + } + return nil +} diff --git a/internal/model/user/user.go b/internal/model/user/user.go index 3344745..ee9e6f9 100644 --- a/internal/model/user/user.go +++ b/internal/model/user/user.go @@ -25,6 +25,7 @@ type User struct { EnableTradeNotify *bool `gorm:"default:false;not null;comment:Enable Trade Notifications"` AuthMethods []AuthMethods `gorm:"foreignKey:UserId;references:Id"` UserDevices []Device `gorm:"foreignKey:UserId;references:Id"` + Rules string `gorm:"type:TEXT;comment:User Rules"` CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` UpdatedAt time.Time `gorm:"comment:Update Time"` } diff --git a/internal/types/types.go b/internal/types/types.go index 1c54b3f..f039294 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -1577,7 +1577,7 @@ type QueryIPLocationRequest struct { type QueryIPLocationResponse struct { Country string `json:"country"` - Region string `json:"regio,omitempty"` + Region string `json:"region,omitempty"` City string `json:"city"` } @@ -2479,6 +2479,10 @@ type UpdateUserPasswordRequest struct { Password string `json:"password" validate:"required"` } +type UpdateUserRulesRequest struct { + Rules []string `json:"rules" validate:"required"` +} + type UpdateUserSubscribeNoteRequest struct { UserSubscribeId int64 `json:"user_subscribe_id" validate:"required"` Note string `json:"note" validate:"max=500"` @@ -2517,6 +2521,7 @@ type User struct { EnableTradeNotify bool `json:"enable_trade_notify"` AuthMethods []UserAuthMethod `json:"auth_methods"` UserDevices []UserDevice `json:"user_devices"` + Rules []string `json:"rules"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` DeletedAt int64 `json:"deleted_at,omitempty"` @@ -2608,7 +2613,6 @@ type UserSubscribe struct { Token string `json:"token"` Status uint8 `json:"status"` Short string `json:"short"` - Note string `json:"note"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` } @@ -2628,7 +2632,6 @@ type UserSubscribeDetail struct { Upload int64 `json:"upload"` Token string `json:"token"` Status uint8 `json:"status"` - Note string `json:"note"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` } @@ -2647,7 +2650,6 @@ type UserSubscribeInfo struct { Upload int64 `json:"upload"` Token string `json:"token"` Status uint8 `json:"status"` - Note string `json:"note"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` IsTryOut bool `json:"is_try_out"` From 5c2d0be8e2ab42a78b01c1bc0b222332bf37f403 Mon Sep 17 00:00:00 2001 From: Chang lue Tsen Date: Mon, 24 Nov 2025 10:43:10 -0500 Subject: [PATCH 17/50] feat(adapter): add additional protocol parameters for enhanced configuration --- adapter/adapter.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/adapter/adapter.go b/adapter/adapter.go index 1d6e4f4..da5d049 100644 --- a/adapter/adapter.go +++ b/adapter/adapter.go @@ -127,12 +127,12 @@ func (adapter *Adapter) Proxies(servers []*node.Node) ([]Proxy, error) { HopPorts: protocol.HopPorts, HopInterval: protocol.HopInterval, ObfsPassword: protocol.ObfsPassword, + UpMbps: protocol.UpMbps, + DownMbps: protocol.DownMbps, DisableSNI: protocol.DisableSNI, ReduceRtt: protocol.ReduceRtt, UDPRelayMode: protocol.UDPRelayMode, CongestionController: protocol.CongestionController, - UpMbps: protocol.UpMbps, - DownMbps: protocol.DownMbps, PaddingScheme: protocol.PaddingScheme, Multiplex: protocol.Multiplex, XhttpMode: protocol.XhttpMode, @@ -145,6 +145,10 @@ func (adapter *Adapter) Proxies(servers []*node.Node) ([]Proxy, error) { EncryptionPrivateKey: protocol.EncryptionPrivateKey, EncryptionClientPadding: protocol.EncryptionClientPadding, EncryptionPassword: protocol.EncryptionPassword, + Ratio: protocol.Ratio, + CertMode: protocol.CertMode, + CertDNSProvider: protocol.CertDNSProvider, + CertDNSEnv: protocol.CertDNSEnv, }) } } From 7277438b074161aa4be57585d94a47dbccc86e54 Mon Sep 17 00:00:00 2001 From: Chang lue Tsen Date: Wed, 26 Nov 2025 12:13:33 -0500 Subject: [PATCH 18/50] feat(user): add commission withdrawal and query withdrawal log functionality --- apis/public/user.api | 30 +++++++++++++++++++ .../database/02121_user_withdrawal.down.sql | 5 ++++ .../database/02121_user_withdrawal.up.sql | 16 ++++++++++ .../public/user/commissionWithdrawHandler.go | 26 ++++++++++++++++ .../public/user/queryWithdrawalLogHandler.go | 26 ++++++++++++++++ internal/handler/routes.go | 6 ++++ .../public/user/commissionWithdrawLogic.go | 30 +++++++++++++++++++ .../public/user/queryWithdrawalLogLogic.go | 30 +++++++++++++++++++ internal/model/user/user.go | 15 ++++++++++ internal/types/types.go | 26 ++++++++++++++++ 10 files changed, 210 insertions(+) create mode 100644 initialize/migrate/database/02121_user_withdrawal.down.sql create mode 100644 initialize/migrate/database/02121_user_withdrawal.up.sql create mode 100644 internal/handler/public/user/commissionWithdrawHandler.go create mode 100644 internal/handler/public/user/queryWithdrawalLogHandler.go create mode 100644 internal/logic/public/user/commissionWithdrawLogic.go create mode 100644 internal/logic/public/user/queryWithdrawalLogLogic.go diff --git a/apis/public/user.api b/apis/public/user.api index 45ebdef..f37eef3 100644 --- a/apis/public/user.api +++ b/apis/public/user.api @@ -111,6 +111,28 @@ type ( UpdateUserRulesRequest { Rules []string `json:"rules" validate:"required"` } + CommissionWithdrawRequest { + Amount int64 `json:"amount"` + Content string `json:"content"` + } + WithdrawalLog { + Id int64 `json:"id"` + UserId int64 `json:"user_id"` + Amount int64 `json:"amount"` + Content string `json:"content"` + Status uint8 `json:"status"` + Reason string `json:"reason,omitempty"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + } + QueryWithdrawalLogListRequest { + Page int `form:"page"` + Size int `form:"size"` + } + QueryWithdrawalLogListResponse { + List []WithdrawalLog `json:"list"` + Total int64 `json:"total"` + } ) @server ( @@ -222,5 +244,13 @@ service ppanel { @doc "Update User Rules" @handler UpdateUserRules put /rules (UpdateUserRulesRequest) + + @doc "Commission Withdraw" + @handler CommissionWithdraw + post /commission_withdraw (CommissionWithdrawRequest) returns (WithdrawalLog) + + @doc "Query Withdrawal Log" + @handler QueryWithdrawalLog + get /withdrawal_log (QueryWithdrawalLogListRequest) returns (QueryWithdrawalLogListResponse) } diff --git a/initialize/migrate/database/02121_user_withdrawal.down.sql b/initialize/migrate/database/02121_user_withdrawal.down.sql new file mode 100644 index 0000000..4de8bc5 --- /dev/null +++ b/initialize/migrate/database/02121_user_withdrawal.down.sql @@ -0,0 +1,5 @@ +DROP TABLE IF EXISTS `withdrawals`; + +DELETE FROM `system` +WHERE `category` = 'invite' + AND `key` = 'WithdrawalMethod'; diff --git a/initialize/migrate/database/02121_user_withdrawal.up.sql b/initialize/migrate/database/02121_user_withdrawal.up.sql new file mode 100644 index 0000000..4f39e1e --- /dev/null +++ b/initialize/migrate/database/02121_user_withdrawal.up.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS `withdrawals` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'Primary Key', + `user_id` BIGINT NOT NULL COMMENT 'User ID', + `amount` BIGINT NOT NULL COMMENT 'Withdrawal Amount', + `content` TEXT COMMENT 'Withdrawal Content', + `status` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Withdrawal Status', + `reason` VARCHAR(500) NOT NULL DEFAULT '' COMMENT 'Rejection Reason', + `created_at` DATETIME NOT NULL COMMENT 'Creation Time', + `updated_at` DATETIME NOT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +INSERT IGNORE INTO `system` (`category`, `key`, `value`, `type`, `desc`, `created_at`, `updated_at`) +VALUES + ('invite', 'WithdrawalMethod', '', 'string', 'withdrawal method', '2025-04-22 14:25:16.637', '2025-04-22 14:25:16.637'); \ No newline at end of file diff --git a/internal/handler/public/user/commissionWithdrawHandler.go b/internal/handler/public/user/commissionWithdrawHandler.go new file mode 100644 index 0000000..f4f244c --- /dev/null +++ b/internal/handler/public/user/commissionWithdrawHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/public/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Commission Withdraw +func CommissionWithdrawHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.CommissionWithdrawRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewCommissionWithdrawLogic(c.Request.Context(), svcCtx) + resp, err := l.CommissionWithdraw(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/user/queryWithdrawalLogHandler.go b/internal/handler/public/user/queryWithdrawalLogHandler.go new file mode 100644 index 0000000..9f0bddc --- /dev/null +++ b/internal/handler/public/user/queryWithdrawalLogHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/public/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Query Withdrawal Log +func QueryWithdrawalLogHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.QueryWithdrawalLogListRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewQueryWithdrawalLogLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryWithdrawalLog(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/routes.go b/internal/handler/routes.go index 7faf037..e3ad493 100644 --- a/internal/handler/routes.go +++ b/internal/handler/routes.go @@ -807,6 +807,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Query User Commission Log publicUserGroupRouter.GET("/commission_log", publicUser.QueryUserCommissionLogHandler(serverCtx)) + // Commission Withdraw + publicUserGroupRouter.POST("/commission_withdraw", publicUser.CommissionWithdrawHandler(serverCtx)) + // Get Device List publicUserGroupRouter.GET("/devices", publicUser.GetDeviceListHandler(serverCtx)) @@ -857,6 +860,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Verify Email publicUserGroupRouter.POST("/verify_email", publicUser.VerifyEmailHandler(serverCtx)) + + // Query Withdrawal Log + publicUserGroupRouter.GET("/withdrawal_log", publicUser.QueryWithdrawalLogHandler(serverCtx)) } serverGroupRouter := router.Group("/v1/server") diff --git a/internal/logic/public/user/commissionWithdrawLogic.go b/internal/logic/public/user/commissionWithdrawLogic.go new file mode 100644 index 0000000..0fe7e90 --- /dev/null +++ b/internal/logic/public/user/commissionWithdrawLogic.go @@ -0,0 +1,30 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" +) + +type CommissionWithdrawLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Commission Withdraw +func NewCommissionWithdrawLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CommissionWithdrawLogic { + return &CommissionWithdrawLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CommissionWithdrawLogic) CommissionWithdraw(req *types.CommissionWithdrawRequest) (resp *types.WithdrawalLog, err error) { + // todo: add your logic here and delete this line + + return +} diff --git a/internal/logic/public/user/queryWithdrawalLogLogic.go b/internal/logic/public/user/queryWithdrawalLogLogic.go new file mode 100644 index 0000000..1b1a583 --- /dev/null +++ b/internal/logic/public/user/queryWithdrawalLogLogic.go @@ -0,0 +1,30 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" +) + +type QueryWithdrawalLogLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewQueryWithdrawalLogLogic Query Withdrawal Log +func NewQueryWithdrawalLogLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryWithdrawalLogLogic { + return &QueryWithdrawalLogLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryWithdrawalLogLogic) QueryWithdrawalLog(req *types.QueryWithdrawalLogListRequest) (resp *types.QueryWithdrawalLogListResponse, err error) { + // todo: add your logic here and delete this line + + return +} diff --git a/internal/model/user/user.go b/internal/model/user/user.go index ee9e6f9..923594e 100644 --- a/internal/model/user/user.go +++ b/internal/model/user/user.go @@ -102,3 +102,18 @@ type DeviceOnlineRecord struct { func (DeviceOnlineRecord) TableName() string { return "user_device_online_record" } + +type Withdrawal struct { + Id int64 `gorm:"primaryKey"` + UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"` + Amount int64 `gorm:"not null;comment:Withdrawal Amount"` + Content string `gorm:"type:text;comment:Withdrawal Content"` + Status uint8 `gorm:"type:tinyint(1);default:0;comment:Withdrawal Status: 0: Pending 1: Approved 2: Rejected"` + Reason string `gorm:"type:varchar(500);default:'';comment:Rejection Reason"` + CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` +} + +func (*Withdrawal) TableName() string { + return "user_withdrawal" +} diff --git a/internal/types/types.go b/internal/types/types.go index f039294..565c4ce 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -239,6 +239,11 @@ type CommissionLog struct { Timestamp int64 `json:"timestamp"` } +type CommissionWithdrawRequest struct { + Amount int64 `json:"amount"` + Content string `json:"content"` +} + type Coupon struct { Id int64 `json:"id"` Name string `json:"name"` @@ -1723,6 +1728,16 @@ type QueryUserSubscribeNodeListResponse struct { List []UserSubscribeInfo `json:"list"` } +type QueryWithdrawalLogListRequest struct { + Page int `form:"page"` + Size int `form:"size"` +} + +type QueryWithdrawalLogListResponse struct { + List []WithdrawalLog `json:"list"` + Total int64 `json:"total"` +} + type QuotaTask struct { Id int64 `json:"id"` Subscribers []int64 `json:"subscribers"` @@ -2766,3 +2781,14 @@ type VmessProtocol struct { Network string `json:"network"` Transport string `json:"transport"` } + +type WithdrawalLog struct { + Id int64 `json:"id"` + UserId int64 `json:"user_id"` + Amount int64 `json:"amount"` + Content string `json:"content"` + Status uint8 `json:"status"` + Reason string `json:"reason,omitempty"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} From 143445a2fc1114404bec6e62a39544d9adb87b54 Mon Sep 17 00:00:00 2001 From: Tension Date: Sat, 29 Nov 2025 14:33:05 +0800 Subject: [PATCH 19/50] feat(commission): implement commission withdrawal logic and logging --- .../public/user/commissionWithdrawLogic.go | 82 ++++++++++++++++++- internal/model/log/log.go | 35 ++++---- pkg/xerr/errCode.go | 19 +++-- 3 files changed, 108 insertions(+), 28 deletions(-) diff --git a/internal/logic/public/user/commissionWithdrawLogic.go b/internal/logic/public/user/commissionWithdrawLogic.go index 0fe7e90..d16dec0 100644 --- a/internal/logic/public/user/commissionWithdrawLogic.go +++ b/internal/logic/public/user/commissionWithdrawLogic.go @@ -2,10 +2,16 @@ package user import ( "context" + "time" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/model/user" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/constant" "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" ) type CommissionWithdrawLogic struct { @@ -24,7 +30,79 @@ func NewCommissionWithdrawLogic(ctx context.Context, svcCtx *svc.ServiceContext) } func (l *CommissionWithdrawLogic) CommissionWithdraw(req *types.CommissionWithdrawRequest) (resp *types.WithdrawalLog, err error) { - // todo: add your logic here and delete this line + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } - return + if u.Commission < req.Amount { + logger.Errorf("User %d has insufficient commission balance: %.2f, requested: %.2f", u.Id, float64(u.Commission)/100, float64(req.Amount)/100) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserCommissionNotEnough), "User %d has insufficient commission balance", u.Id) + } + + tx := l.svcCtx.DB.WithContext(l.ctx).Begin() + + // update user commission balance + u.Commission -= req.Amount + if err = l.svcCtx.UserModel.Update(l.ctx, u, tx); err != nil { + tx.Rollback() + l.Errorf("Failed to update user %d commission balance: %v", u.Id, err) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "Failed to update user %d commission balance: %v", u.Id, err) + } + + // create withdrawal log + logInfo := log.Commission{ + Type: log.CommissionTypeConvertBalance, + Amount: req.Amount, + Timestamp: time.Now().UnixMilli(), + } + b, err := logInfo.Marshal() + + if err != nil { + tx.Rollback() + l.Errorf("Failed to marshal commission log for user %d: %v", u.Id, err) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Failed to marshal commission log for user %d: %v", u.Id, err) + } + + err = tx.Model(log.SystemLog{}).Create(&log.SystemLog{ + Type: log.TypeCommission.Uint8(), + Date: time.Now().Format("2006-01-02"), + ObjectID: u.Id, + Content: string(b), + CreatedAt: time.Now(), + }).Error + + if err != nil { + tx.Rollback() + l.Errorf("Failed to create commission log for user %d: %v", u.Id, err) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "Failed to create commission log for user %d: %v", u.Id, err) + } + + err = tx.Model(&user.Withdrawal{}).Create(&user.Withdrawal{ + UserId: u.Id, + Amount: req.Amount, + Content: req.Content, + Status: 0, + Reason: "", + }).Error + + if err != nil { + tx.Rollback() + l.Errorf("Failed to create withdrawal log for user %d: %v", u.Id, err) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "Failed to create withdrawal log for user %d: %v", u.Id, err) + } + if err = tx.Commit().Error; err != nil { + l.Errorf("Transaction commit failed for user %d withdrawal: %v", u.Id, err) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Transaction commit failed for user %d withdrawal: %v", u.Id, err) + } + + return &types.WithdrawalLog{ + UserId: u.Id, + Amount: req.Amount, + Content: req.Content, + Status: 0, + Reason: "", + CreatedAt: time.Now().UnixMilli(), + }, nil } diff --git a/internal/model/log/log.go b/internal/model/log/log.go index af3eb98..1afd13d 100644 --- a/internal/model/log/log.go +++ b/internal/model/log/log.go @@ -33,23 +33,24 @@ const ( TypeTrafficStat Type = 42 // Daily traffic statistics log ) const ( - ResetSubscribeTypeAuto uint16 = 231 // Auto reset - ResetSubscribeTypeAdvance uint16 = 232 // Advance reset - ResetSubscribeTypePaid uint16 = 233 // Paid reset - ResetSubscribeTypeQuota uint16 = 234 // Quota reset - BalanceTypeRecharge uint16 = 321 // Recharge - BalanceTypeWithdraw uint16 = 322 // Withdraw - BalanceTypePayment uint16 = 323 // Payment - BalanceTypeRefund uint16 = 324 // Refund - BalanceTypeAdjust uint16 = 326 // Admin Adjust - BalanceTypeReward uint16 = 325 // Reward - CommissionTypePurchase uint16 = 331 // Purchase - CommissionTypeRenewal uint16 = 332 // Renewal - CommissionTypeRefund uint16 = 333 // Refund - commissionTypeWithdraw uint16 = 334 // withdraw - CommissionTypeAdjust uint16 = 335 // Admin Adjust - GiftTypeIncrease uint16 = 341 // Increase - GiftTypeReduce uint16 = 342 // Reduce + ResetSubscribeTypeAuto uint16 = 231 // Auto reset + ResetSubscribeTypeAdvance uint16 = 232 // Advance reset + ResetSubscribeTypePaid uint16 = 233 // Paid reset + ResetSubscribeTypeQuota uint16 = 234 // Quota reset + BalanceTypeRecharge uint16 = 321 // Recharge + BalanceTypeWithdraw uint16 = 322 // Withdraw + BalanceTypePayment uint16 = 323 // Payment + BalanceTypeRefund uint16 = 324 // Refund + BalanceTypeAdjust uint16 = 326 // Admin Adjust + BalanceTypeReward uint16 = 325 // Reward + CommissionTypePurchase uint16 = 331 // Purchase + CommissionTypeRenewal uint16 = 332 // Renewal + CommissionTypeRefund uint16 = 333 // Refund + CommissionTypeWithdraw uint16 = 334 // withdraw + CommissionTypeAdjust uint16 = 335 // Admin Adjust + CommissionTypeConvertBalance uint16 = 336 // Convert to Balance + GiftTypeIncrease uint16 = 341 // Increase + GiftTypeReduce uint16 = 342 // Reduce ) // Uint8 converts Type to uint8. diff --git a/pkg/xerr/errCode.go b/pkg/xerr/errCode.go index c9377c4..7e1bfd3 100644 --- a/pkg/xerr/errCode.go +++ b/pkg/xerr/errCode.go @@ -18,15 +18,16 @@ const ( // User error const ( - UserExist uint32 = 20001 - UserNotExist uint32 = 20002 - UserPasswordError uint32 = 20003 - UserDisabled uint32 = 20004 - InsufficientBalance uint32 = 20005 - StopRegister uint32 = 20006 - TelegramNotBound uint32 = 20007 - UserNotBindOauth uint32 = 20008 - InviteCodeError uint32 = 20009 + UserExist uint32 = 20001 + UserNotExist uint32 = 20002 + UserPasswordError uint32 = 20003 + UserDisabled uint32 = 20004 + InsufficientBalance uint32 = 20005 + StopRegister uint32 = 20006 + TelegramNotBound uint32 = 20007 + UserNotBindOauth uint32 = 20008 + InviteCodeError uint32 = 20009 + UserCommissionNotEnough uint32 = 20010 ) // Node error From e18809f9b7cbcdbd1b5824e2bde7dfcc2b2bc7fc Mon Sep 17 00:00:00 2001 From: Chang lue Tsen Date: Sun, 30 Nov 2025 10:28:11 -0500 Subject: [PATCH 20/50] feat(report): update registration response structure and enhance error logging --- internal/report/report.go | 8 ++++++-- internal/report/tool.go | 5 ++++- internal/report/types.go | 6 ------ internal/server.go | 3 ++- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/internal/report/report.go b/internal/report/report.go index 8b7faa9..1ba618a 100644 --- a/internal/report/report.go +++ b/internal/report/report.go @@ -6,6 +6,10 @@ const ( // RegisterResponse 模块注册响应参数 type RegisterResponse struct { - Success bool `json:"success"` // 注册是否成功 - Message string `json:"message"` // 返回信息 + Code int `json:"code"` // 响应代码 + Message string `json:"message"` // 响应信息 + Data struct { + Success bool `json:"success"` // 注册是否成功 + Message string `json:"message"` // 返回信息 + } `json:"data"` // 响应数据 } diff --git a/internal/report/tool.go b/internal/report/tool.go index cd57f1e..fe8a68c 100644 --- a/internal/report/tool.go +++ b/internal/report/tool.go @@ -59,6 +59,7 @@ func RegisterModule(port int) error { // 从环境变量中读取网关模块端口 gatewayPort, err := GatewayPort() if err != nil { + logger.Errorf("Failed to determine GATEWAY_PORT: %v", err) return err } @@ -82,6 +83,7 @@ func RegisterModule(port int) error { }).SetResult(&response).Post(RegisterAPI) if err != nil { + logger.Errorf("Failed to register service: %v", err) return err } @@ -89,7 +91,8 @@ func RegisterModule(port int) error { return errors.New("failed to register module: " + result.Status()) } - if !response.Success { + if !response.Data.Success { + logger.Infof("Result: %v", result.String()) return errors.New("failed to register module: " + response.Message) } logger.Infof("Module registered successfully: %s", response.Message) diff --git a/internal/report/types.go b/internal/report/types.go index 6abf9b2..d9cd643 100644 --- a/internal/report/types.go +++ b/internal/report/types.go @@ -1,11 +1,5 @@ package report -// RegisterServiceResponse 模块注册请求参数 -type RegisterServiceResponse struct { - Success bool `json:"success"` // 注册是否成功 - Message string `json:"message"` // 返回信息 -} - type RegisterServiceRequest struct { Secret string `json:"secret"` // 通讯密钥 ProxyPath string `json:"proxy_path"` // 代理路径 diff --git a/internal/server.go b/internal/server.go index 47969be..78d6422 100644 --- a/internal/server.go +++ b/internal/server.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "net/http" + "os" "time" "github.com/perfect-panel/server/internal/report" @@ -86,7 +87,7 @@ func (m *Service) Start() { err = report.RegisterModule(port) if err != nil { logger.Errorf("register module error: %s", err.Error()) - panic(err) + os.Exit(1) } logger.Infof("module registered on port %d", port) } From 4cd24e7600a9f88ea5d7a7ed29f50214685173f9 Mon Sep 17 00:00:00 2001 From: Chang lue Tsen Date: Sun, 30 Nov 2025 11:02:02 -0500 Subject: [PATCH 21/50] feat(swagger): add basepath to Swagger file generation for improved API routing --- .github/workflows/swagger.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/swagger.yaml b/.github/workflows/swagger.yaml index 177c2b0..57c8561 100644 --- a/.github/workflows/swagger.yaml +++ b/.github/workflows/swagger.yaml @@ -33,11 +33,11 @@ jobs: - name: Generate Swagger file run: | mkdir -p swagger - goctl api plugin -plugin goctl-swagger='swagger -filename common.json -pack Response -response "[{\"name\":\"code\",\"type\":\"integer\",\"description\":\"状态码\"},{\"name\":\"msg\",\"type\":\"string\",\"description\":\"消息\"},{\"name\":\"data\",\"type\":\"object\",\"description\":\"数据\",\"is_data\":true}]";' -api ./apis/swagger_common.api -dir ./swagger - goctl api plugin -plugin goctl-swagger='swagger -filename user.json -pack Response -response "[{\"name\":\"code\",\"type\":\"integer\",\"description\":\"状态码\"},{\"name\":\"msg\",\"type\":\"string\",\"description\":\"消息\"},{\"name\":\"data\",\"type\":\"object\",\"description\":\"数据\",\"is_data\":true}]";' -api ./apis/swagger_user.api -dir ./swagger - goctl api plugin -plugin goctl-swagger='swagger -filename admin.json -pack Response -response "[{\"name\":\"code\",\"type\":\"integer\",\"description\":\"状态码\"},{\"name\":\"msg\",\"type\":\"string\",\"description\":\"消息\"},{\"name\":\"data\",\"type\":\"object\",\"description\":\"数据\",\"is_data\":true}]";' -api ./apis/swagger_admin.api -dir ./swagger - goctl api plugin -plugin goctl-swagger='swagger -filename ppanel.json -pack Response -response "[{\"name\":\"code\",\"type\":\"integer\",\"description\":\"状态码\"},{\"name\":\"msg\",\"type\":\"string\",\"description\":\"消息\"},{\"name\":\"data\",\"type\":\"object\",\"description\":\"数据\",\"is_data\":true}]";' -api ppanel.api -dir ./swagger - goctl api plugin -plugin goctl-swagger='swagger -filename node.json -pack Response -response "[{\"name\":\"code\",\"type\":\"integer\",\"description\":\"状态码\"},{\"name\":\"msg\",\"type\":\"string\",\"description\":\"消息\"},{\"name\":\"data\",\"type\":\"object\",\"description\":\"数据\",\"is_data\":true}]";' -api ./apis/swagger_node.api -dir ./swagger + goctl api plugin -plugin goctl-swagger='swagger -filename common.json -pack Response -response "[{\"name\":\"code\",\"type\":\"integer\",\"description\":\"状态码\"},{\"name\":\"msg\",\"type\":\"string\",\"description\":\"消息\"},{\"name\":\"data\",\"type\":\"object\",\"description\":\"数据\",\"is_data\":true}]";' -basepath /api -api ./apis/swagger_common.api -dir ./swagger + goctl api plugin -plugin goctl-swagger='swagger -filename user.json -pack Response -response "[{\"name\":\"code\",\"type\":\"integer\",\"description\":\"状态码\"},{\"name\":\"msg\",\"type\":\"string\",\"description\":\"消息\"},{\"name\":\"data\",\"type\":\"object\",\"description\":\"数据\",\"is_data\":true}]";' -basepath /api -api ./apis/swagger_user.api -dir ./swagger + goctl api plugin -plugin goctl-swagger='swagger -filename admin.json -pack Response -response "[{\"name\":\"code\",\"type\":\"integer\",\"description\":\"状态码\"},{\"name\":\"msg\",\"type\":\"string\",\"description\":\"消息\"},{\"name\":\"data\",\"type\":\"object\",\"description\":\"数据\",\"is_data\":true}]";' -basepath /api -api ./apis/swagger_admin.api -dir ./swagger + goctl api plugin -plugin goctl-swagger='swagger -filename ppanel.json -pack Response -response "[{\"name\":\"code\",\"type\":\"integer\",\"description\":\"状态码\"},{\"name\":\"msg\",\"type\":\"string\",\"description\":\"消息\"},{\"name\":\"data\",\"type\":\"object\",\"description\":\"数据\",\"is_data\":true}]";' -basepath /api -api ppanel.api -dir ./swagger + goctl api plugin -plugin goctl-swagger='swagger -filename node.json -pack Response -response "[{\"name\":\"code\",\"type\":\"integer\",\"description\":\"状态码\"},{\"name\":\"msg\",\"type\":\"string\",\"description\":\"消息\"},{\"name\":\"data\",\"type\":\"object\",\"description\":\"数据\",\"is_data\":true}]";' -basepath /api -api ./apis/swagger_node.api -dir ./swagger - name: Verify Swagger file From f1794b26b1aedd790201cb88de66bf27d1f713cf Mon Sep 17 00:00:00 2001 From: Chang lue Tsen Date: Sun, 30 Nov 2025 11:07:17 -0500 Subject: [PATCH 22/50] revert(swagger): remove basepath from Swagger file generation for improved compatibility --- .github/workflows/swagger.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/swagger.yaml b/.github/workflows/swagger.yaml index 57c8561..177c2b0 100644 --- a/.github/workflows/swagger.yaml +++ b/.github/workflows/swagger.yaml @@ -33,11 +33,11 @@ jobs: - name: Generate Swagger file run: | mkdir -p swagger - goctl api plugin -plugin goctl-swagger='swagger -filename common.json -pack Response -response "[{\"name\":\"code\",\"type\":\"integer\",\"description\":\"状态码\"},{\"name\":\"msg\",\"type\":\"string\",\"description\":\"消息\"},{\"name\":\"data\",\"type\":\"object\",\"description\":\"数据\",\"is_data\":true}]";' -basepath /api -api ./apis/swagger_common.api -dir ./swagger - goctl api plugin -plugin goctl-swagger='swagger -filename user.json -pack Response -response "[{\"name\":\"code\",\"type\":\"integer\",\"description\":\"状态码\"},{\"name\":\"msg\",\"type\":\"string\",\"description\":\"消息\"},{\"name\":\"data\",\"type\":\"object\",\"description\":\"数据\",\"is_data\":true}]";' -basepath /api -api ./apis/swagger_user.api -dir ./swagger - goctl api plugin -plugin goctl-swagger='swagger -filename admin.json -pack Response -response "[{\"name\":\"code\",\"type\":\"integer\",\"description\":\"状态码\"},{\"name\":\"msg\",\"type\":\"string\",\"description\":\"消息\"},{\"name\":\"data\",\"type\":\"object\",\"description\":\"数据\",\"is_data\":true}]";' -basepath /api -api ./apis/swagger_admin.api -dir ./swagger - goctl api plugin -plugin goctl-swagger='swagger -filename ppanel.json -pack Response -response "[{\"name\":\"code\",\"type\":\"integer\",\"description\":\"状态码\"},{\"name\":\"msg\",\"type\":\"string\",\"description\":\"消息\"},{\"name\":\"data\",\"type\":\"object\",\"description\":\"数据\",\"is_data\":true}]";' -basepath /api -api ppanel.api -dir ./swagger - goctl api plugin -plugin goctl-swagger='swagger -filename node.json -pack Response -response "[{\"name\":\"code\",\"type\":\"integer\",\"description\":\"状态码\"},{\"name\":\"msg\",\"type\":\"string\",\"description\":\"消息\"},{\"name\":\"data\",\"type\":\"object\",\"description\":\"数据\",\"is_data\":true}]";' -basepath /api -api ./apis/swagger_node.api -dir ./swagger + goctl api plugin -plugin goctl-swagger='swagger -filename common.json -pack Response -response "[{\"name\":\"code\",\"type\":\"integer\",\"description\":\"状态码\"},{\"name\":\"msg\",\"type\":\"string\",\"description\":\"消息\"},{\"name\":\"data\",\"type\":\"object\",\"description\":\"数据\",\"is_data\":true}]";' -api ./apis/swagger_common.api -dir ./swagger + goctl api plugin -plugin goctl-swagger='swagger -filename user.json -pack Response -response "[{\"name\":\"code\",\"type\":\"integer\",\"description\":\"状态码\"},{\"name\":\"msg\",\"type\":\"string\",\"description\":\"消息\"},{\"name\":\"data\",\"type\":\"object\",\"description\":\"数据\",\"is_data\":true}]";' -api ./apis/swagger_user.api -dir ./swagger + goctl api plugin -plugin goctl-swagger='swagger -filename admin.json -pack Response -response "[{\"name\":\"code\",\"type\":\"integer\",\"description\":\"状态码\"},{\"name\":\"msg\",\"type\":\"string\",\"description\":\"消息\"},{\"name\":\"data\",\"type\":\"object\",\"description\":\"数据\",\"is_data\":true}]";' -api ./apis/swagger_admin.api -dir ./swagger + goctl api plugin -plugin goctl-swagger='swagger -filename ppanel.json -pack Response -response "[{\"name\":\"code\",\"type\":\"integer\",\"description\":\"状态码\"},{\"name\":\"msg\",\"type\":\"string\",\"description\":\"消息\"},{\"name\":\"data\",\"type\":\"object\",\"description\":\"数据\",\"is_data\":true}]";' -api ppanel.api -dir ./swagger + goctl api plugin -plugin goctl-swagger='swagger -filename node.json -pack Response -response "[{\"name\":\"code\",\"type\":\"integer\",\"description\":\"状态码\"},{\"name\":\"msg\",\"type\":\"string\",\"description\":\"消息\"},{\"name\":\"data\",\"type\":\"object\",\"description\":\"数据\",\"is_data\":true}]";' -api ./apis/swagger_node.api -dir ./swagger - name: Verify Swagger file From 338d962618581e10da23d233d5d8d6d8ca96f80b Mon Sep 17 00:00:00 2001 From: Tension Date: Mon, 1 Dec 2025 19:01:24 +0800 Subject: [PATCH 23/50] fix(api): remove default value for size in QueryAnnouncementRequest --- apis/types.api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apis/types.api b/apis/types.api index 0efa991..8b4a9aa 100644 --- a/apis/types.api +++ b/apis/types.api @@ -656,7 +656,7 @@ type ( // public announcement QueryAnnouncementRequest { Page int `form:"page"` - Size int `form:"size,default=15"` + Size int `form:"size"` Pinned *bool `form:"pinned"` Popup *bool `form:"popup"` } From 5d632608ab5ccbca8dc831825b088c58b91b5086 Mon Sep 17 00:00:00 2001 From: Tension Date: Mon, 8 Dec 2025 15:47:45 +0800 Subject: [PATCH 24/50] fix(purchase): update notification URL construction for gateway mode support --- .../public/portal/purchaseCheckoutLogic.go | 35 ++++++++++++++++--- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/internal/logic/public/portal/purchaseCheckoutLogic.go b/internal/logic/public/portal/purchaseCheckoutLogic.go index f016a49..ff7faf2 100644 --- a/internal/logic/public/portal/purchaseCheckoutLogic.go +++ b/internal/logic/public/portal/purchaseCheckoutLogic.go @@ -7,6 +7,7 @@ import ( "time" "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/report" "github.com/perfect-panel/server/pkg/constant" paymentPlatform "github.com/perfect-panel/server/pkg/payment" @@ -275,16 +276,29 @@ func (l *PurchaseCheckoutLogic) epayPayment(config *payment.Payment, info *order return "", err } + // gateway mod + + isGatewayMod := report.IsGatewayMode() + // Build notification URL for payment status callbacks notifyUrl := "" if config.Domain != "" { - notifyUrl = config.Domain + "/v1/notify/" + config.Platform + "/" + config.Token + notifyUrl = config.Domain + if isGatewayMod { + notifyUrl += "/api/" + } + notifyUrl = notifyUrl + "/v1/notify/" + config.Platform + "/" + config.Token } else { host, ok := l.ctx.Value(constant.CtxKeyRequestHost).(string) if !ok { host = l.svcCtx.Config.Host } - notifyUrl = "https://" + host + "/v1/notify/" + config.Platform + "/" + config.Token + + notifyUrl = "https://" + host + if isGatewayMod { + notifyUrl += "/api" + } + notifyUrl = notifyUrl + "/v1/notify/" + config.Platform + "/" + config.Token } // Create payment URL for user redirection @@ -317,18 +331,29 @@ func (l *PurchaseCheckoutLogic) CryptoSaaSPayment(config *payment.Payment, info return "", err } + // gateway mod + isGatewayMod := report.IsGatewayMode() + // Build notification URL for payment status callbacks notifyUrl := "" if config.Domain != "" { - notifyUrl = config.Domain + "/v1/notify/" + config.Platform + "/" + config.Token + notifyUrl = config.Domain + if isGatewayMod { + notifyUrl += "/api/" + } + notifyUrl = notifyUrl + "/v1/notify/" + config.Platform + "/" + config.Token } else { host, ok := l.ctx.Value(constant.CtxKeyRequestHost).(string) if !ok { host = l.svcCtx.Config.Host } - notifyUrl = "https://" + host + "/v1/notify/" + config.Platform + "/" + config.Token - } + notifyUrl = "https://" + host + if isGatewayMod { + notifyUrl += "/api" + } + notifyUrl = notifyUrl + "/v1/notify/" + config.Platform + "/" + config.Token + } // Create payment URL for user redirection url := client.CreatePayUrl(epay.Order{ Name: l.svcCtx.Config.Site.SiteName, From 0e7cbf439641eb5efc04feff8786f1a108c787f8 Mon Sep 17 00:00:00 2001 From: Tension Date: Mon, 8 Dec 2025 16:09:21 +0800 Subject: [PATCH 25/50] fix(payment): update notification URL construction for gateway mode support --- .../payment/getPaymentMethodListLogic.go | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/internal/logic/admin/payment/getPaymentMethodListLogic.go b/internal/logic/admin/payment/getPaymentMethodListLogic.go index 77c7e40..ef987f3 100644 --- a/internal/logic/admin/payment/getPaymentMethodListLogic.go +++ b/internal/logic/admin/payment/getPaymentMethodListLogic.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" + "github.com/perfect-panel/server/internal/report" paymentPlatform "github.com/perfect-panel/server/pkg/payment" "github.com/perfect-panel/server/internal/model/payment" @@ -43,15 +44,31 @@ func (l *GetPaymentMethodListLogic) GetPaymentMethodList(req *types.GetPaymentMe Total: total, List: make([]types.PaymentMethodDetail, len(list)), } + + // gateway mod + + isGatewayMod := report.IsGatewayMode() + for i, v := range list { config := make(map[string]interface{}) _ = json.Unmarshal([]byte(v.Config), &config) notifyUrl := "" + if paymentPlatform.ParsePlatform(v.Platform) != paymentPlatform.Balance { + notifyUrl = v.Domain if v.Domain != "" { - notifyUrl = v.Domain + "/v1/notify/" + v.Platform + "/" + v.Token + // if is gateway mod, use gateway domain + if isGatewayMod { + notifyUrl += "/api/" + } + notifyUrl += "/v1/notify/" + v.Platform + "/" + v.Token } else { - notifyUrl = "https://" + l.svcCtx.Config.Host + "/v1/notify/" + v.Platform + "/" + v.Token + notifyUrl += "https://" + l.svcCtx.Config.Host + if isGatewayMod { + notifyUrl += "/api/v1/notify/" + v.Platform + "/" + v.Token + } else { + notifyUrl += "/v1/notify/" + v.Platform + "/" + v.Token + } } } resp.List[i] = types.PaymentMethodDetail{ From ec510b66fb2201729c81d123b37f2e56a5d90146 Mon Sep 17 00:00:00 2001 From: Chang lue Tsen Date: Tue, 23 Dec 2025 07:48:03 -0500 Subject: [PATCH 26/50] refactor(server): remove deprecated server types and related methods for cleaner codebase --- apis/admin/server.api | 8 - .../server/hasMigrateSeverNodeHandler.go | 18 - .../admin/server/migrateServerNodeHandler.go | 18 - internal/handler/routes.go | 6 - .../admin/server/hasMigrateSeverNodeLogic.go | 52 --- .../admin/server/migrateServerNodeLogic.go | 338 ------------------ internal/logic/common/getStatLogic.go | 24 +- internal/model/server/default.go | 141 -------- internal/model/server/model.go | 292 --------------- internal/model/server/server.go | 219 ------------ internal/types/types.go | 2 +- 11 files changed, 20 insertions(+), 1098 deletions(-) delete mode 100644 internal/handler/admin/server/hasMigrateSeverNodeHandler.go delete mode 100644 internal/handler/admin/server/migrateServerNodeHandler.go delete mode 100644 internal/logic/admin/server/hasMigrateSeverNodeLogic.go delete mode 100644 internal/logic/admin/server/migrateServerNodeLogic.go delete mode 100644 internal/model/server/default.go delete mode 100644 internal/model/server/model.go delete mode 100644 internal/model/server/server.go diff --git a/apis/admin/server.api b/apis/admin/server.api index 2877427..87d1f7e 100644 --- a/apis/admin/server.api +++ b/apis/admin/server.api @@ -189,14 +189,6 @@ service ppanel { @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) - @doc "Reset server sort" @handler ResetSortWithServer post /server/sort (ResetSortRequest) diff --git a/internal/handler/admin/server/hasMigrateSeverNodeHandler.go b/internal/handler/admin/server/hasMigrateSeverNodeHandler.go deleted file mode 100644 index 6088577..0000000 --- a/internal/handler/admin/server/hasMigrateSeverNodeHandler.go +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 8f8c842..0000000 --- a/internal/handler/admin/server/migrateServerNodeHandler.go +++ /dev/null @@ -1,18 +0,0 @@ -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 e3ad493..2ac18a8 100644 --- a/internal/handler/routes.go +++ b/internal/handler/routes.go @@ -311,12 +311,6 @@ 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/server/hasMigrateSeverNodeLogic.go b/internal/logic/admin/server/hasMigrateSeverNodeLogic.go deleted file mode 100644 index 128b7f6..0000000 --- a/internal/logic/admin/server/hasMigrateSeverNodeLogic.go +++ /dev/null @@ -1,52 +0,0 @@ -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 deleted file mode 100644 index 4f0a497..0000000 --- a/internal/logic/admin/server/migrateServerNodeLogic.go +++ /dev/null @@ -1,338 +0,0 @@ -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{ - Id: info.Id, - 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, - Ratio: float64(info.TrafficRatio), - }) - 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, - Ratio: float64(info.TrafficRatio), - } - 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, - Ratio: float64(info.TrafficRatio), - } - 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, - Ratio: float64(info.TrafficRatio), - } - 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: "hysteria", - 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, - Ratio: float64(info.TrafficRatio), - } - 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, - Ratio: float64(info.TrafficRatio), - } - 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, - Ratio: float64(info.TrafficRatio), - } - 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/common/getStatLogic.go b/internal/logic/common/getStatLogic.go index df14af0..f97223d 100644 --- a/internal/logic/common/getStatLogic.go +++ b/internal/logic/common/getStatLogic.go @@ -11,7 +11,7 @@ import ( "time" "github.com/perfect-panel/server/internal/config" - "github.com/perfect-panel/server/internal/model/server" + "github.com/perfect-panel/server/internal/model/node" "github.com/perfect-panel/server/internal/model/user" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" @@ -57,13 +57,13 @@ func (l *GetStatLogic) GetStat() (resp *types.GetStatResponse, err error) { u = 1 } var n int64 - err = l.svcCtx.DB.Model(&server.Server{}).Where("enable = 1").Count(&n).Error + err = l.svcCtx.DB.Model(&node.Node{}).Where("enabled = 1").Count(&n).Error if err != nil { l.Logger.Error("[GetStatLogic] get server count failed: ", logger.Field("error", err.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get server count failed: %v", err.Error()) } var nodeaddr []string - err = l.svcCtx.DB.Model(&server.Server{}).Where("enable = 1").Pluck("server_addr", &nodeaddr).Error + err = l.svcCtx.DB.Model(&node.Server{}).Pluck("address", &nodeaddr).Error if err != nil { l.Logger.Error("[GetStatLogic] get server_addr failed: ", logger.Field("error", err.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get server_addr failed: %v", err.Error()) @@ -111,9 +111,23 @@ func (l *GetStatLogic) GetStat() (resp *types.GetStatResponse, err error) { } protocolDict := make(map[string]void) var protocol []string - l.svcCtx.DB.Model(&server.Server{}).Where("enable = true").Pluck("protocol", &protocol) + err = l.svcCtx.DB.Model(&node.Node{}).Where("enabled = true").Pluck("protocol", &protocol).Error + if err != nil { + l.Logger.Error("[GetStatLogic] get protocol failed: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get protocol failed: %v", err.Error()) + } + for _, p := range protocol { - protocolDict[p] = v + var protocols []node.Protocol + err = json.Unmarshal([]byte(p), &protocols) + if err != nil { + continue + } + for _, proto := range protocols { + if _, exists := protocolDict[proto.Type]; !exists { + protocolDict[proto.Type] = v + } + } } protocol = nil for p := range protocolDict { diff --git a/internal/model/server/default.go b/internal/model/server/default.go deleted file mode 100644 index b013f1a..0000000 --- a/internal/model/server/default.go +++ /dev/null @@ -1,141 +0,0 @@ -package server - -import ( - "context" - "errors" - "fmt" - - "github.com/perfect-panel/server/pkg/cache" - "github.com/redis/go-redis/v9" - "gorm.io/gorm" -) - -var _ Model = (*customServerModel)(nil) -var ( - cacheServerIdPrefix = "cache:server:id:" -) - -type ( - Model interface { - serverModel - customServerLogicModel - } - serverModel interface { - Insert(ctx context.Context, data *Server, tx ...*gorm.DB) error - FindOne(ctx context.Context, id int64) (*Server, error) - Update(ctx context.Context, data *Server, tx ...*gorm.DB) error - Delete(ctx context.Context, id int64, tx ...*gorm.DB) error - Transaction(ctx context.Context, fn func(db *gorm.DB) error) error - } - - customServerModel struct { - *defaultServerModel - } - defaultServerModel struct { - cache.CachedConn - table string - } -) - -func newServerModel(db *gorm.DB, c *redis.Client) *defaultServerModel { - return &defaultServerModel{ - CachedConn: cache.NewConn(db, c), - table: "`Server`", - } -} - -// NewModel returns a model for the database table. -func NewModel(conn *gorm.DB, c *redis.Client) Model { - return &customServerModel{ - defaultServerModel: newServerModel(conn, c), - } -} - -//nolint:unused -func (m *defaultServerModel) batchGetCacheKeys(Servers ...*Server) []string { - var keys []string - for _, server := range Servers { - keys = append(keys, m.getCacheKeys(server)...) - } - return keys - -} - -func (m *defaultServerModel) getCacheKeys(data *Server) []string { - if data == nil { - return []string{} - } - detailsKey := fmt.Sprintf("%s%v", CacheServerDetailPrefix, data.Id) - ServerIdKey := fmt.Sprintf("%s%v", cacheServerIdPrefix, data.Id) - //configIdKey := fmt.Sprintf("%s%v", config.ServerConfigCacheKey, data.Id) - //userIDKey := fmt.Sprintf("%s%d", config.ServerUserListCacheKey, data.Id) - - // query protocols to get config keys - - cacheKeys := []string{ - ServerIdKey, - detailsKey, - //configIdKey, - //userIDKey, - } - return cacheKeys -} - -func (m *defaultServerModel) Insert(ctx context.Context, data *Server, tx ...*gorm.DB) error { - err := m.ExecCtx(ctx, func(conn *gorm.DB) error { - if len(tx) > 0 { - conn = tx[0] - } - return conn.Create(&data).Error - }, m.getCacheKeys(data)...) - return err -} - -func (m *defaultServerModel) FindOne(ctx context.Context, id int64) (*Server, error) { - ServerIdKey := fmt.Sprintf("%s%v", cacheServerIdPrefix, id) - var resp Server - err := m.QueryCtx(ctx, &resp, ServerIdKey, func(conn *gorm.DB, v interface{}) error { - return conn.Model(&Server{}).Where("`id` = ?", id).First(&resp).Error - }) - switch { - case err == nil: - return &resp, nil - default: - return nil, err - } -} - -func (m *defaultServerModel) Update(ctx context.Context, data *Server, tx ...*gorm.DB) error { - old, err := m.FindOne(ctx, data.Id) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return err - } - err = m.ExecCtx(ctx, func(conn *gorm.DB) error { - if len(tx) > 0 { - conn = tx[0] - } - return conn.Save(data).Error - }, m.getCacheKeys(old)...) - return err -} - -func (m *defaultServerModel) Delete(ctx context.Context, id int64, tx ...*gorm.DB) error { - data, err := m.FindOne(ctx, id) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil - } - return err - } - err = m.ExecCtx(ctx, func(conn *gorm.DB) error { - if len(tx) > 0 { - conn = tx[0] - } - return conn.Delete(&Server{}, id).Error - }, m.getCacheKeys(data)...) - return err -} - -func (m *defaultServerModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error { - return m.TransactCtx(ctx, fn) -} diff --git a/internal/model/server/model.go b/internal/model/server/model.go deleted file mode 100644 index 58ae7b7..0000000 --- a/internal/model/server/model.go +++ /dev/null @@ -1,292 +0,0 @@ -package server - -import ( - "context" - "fmt" - "strings" - - "gorm.io/gorm" -) - -type customServerLogicModel interface { - FindServerListByFilter(ctx context.Context, filter *ServerFilter) (total int64, list []*Server, err error) - ClearCache(ctx context.Context, id int64) error - QueryServerCountByServerGroups(ctx context.Context, groupIds []int64) (int64, error) - QueryAllGroup(ctx context.Context) ([]*Group, error) - BatchDeleteNodeGroup(ctx context.Context, ids []int64) error - InsertGroup(ctx context.Context, data *Group) error - FindOneGroup(ctx context.Context, id int64) (*Group, error) - UpdateGroup(ctx context.Context, data *Group) error - DeleteGroup(ctx context.Context, id int64) error - FindServerDetailByGroupIdsAndIds(ctx context.Context, groupId, ids []int64) ([]*Server, error) - FindServerListByGroupIds(ctx context.Context, groupId []int64) ([]*Server, error) - FindAllServer(ctx context.Context) ([]*Server, error) - FindNodeByServerAddrAndProtocol(ctx context.Context, serverAddr string, protocol string) ([]*Server, error) - FindServerMinSortByIds(ctx context.Context, ids []int64) (int64, error) - FindServerListByIds(ctx context.Context, ids []int64) ([]*Server, error) - InsertRuleGroup(ctx context.Context, data *RuleGroup) error - FindOneRuleGroup(ctx context.Context, id int64) (*RuleGroup, error) - UpdateRuleGroup(ctx context.Context, data *RuleGroup) error - DeleteRuleGroup(ctx context.Context, id int64) error - QueryAllRuleGroup(ctx context.Context) ([]*RuleGroup, error) - FindServersByTag(ctx context.Context, tag string) ([]*Server, error) - FindServerTags(ctx context.Context) ([]string, error) - - SetDefaultRuleGroup(ctx context.Context, id int64) error -} - -var ( - CacheServerDetailPrefix = "cache:server:detail:" - cacheServerGroupAllKeys = "cache:serverGroup:all" - cacheServerRuleGroupAllKeys = "cache:serverRuleGroup:all" -) - -// ClearCache Clear Cache -func (m *customServerModel) ClearCache(ctx context.Context, id int64) error { - serverIdKey := fmt.Sprintf("%s%v", cacheServerIdPrefix, id) - //configKey := fmt.Sprintf("%s%d", config.ServerConfigCacheKey, id) - //userListKey := fmt.Sprintf("%s%v", config.ServerUserListCacheKey, id) - - return m.DelCacheCtx(ctx, serverIdKey) -} - -// QueryServerCountByServerGroups Query Server Count By Server Groups -func (m *customServerModel) QueryServerCountByServerGroups(ctx context.Context, groupIds []int64) (int64, error) { - var count int64 - err := m.QueryNoCacheCtx(ctx, &count, func(conn *gorm.DB, v interface{}) error { - return conn.Model(&Server{}).Where("group_id IN ?", groupIds).Count(&count).Error - }) - return count, err -} - -// QueryAllGroup returns all groups. -func (m *customServerModel) QueryAllGroup(ctx context.Context) ([]*Group, error) { - var groups []*Group - err := m.QueryCtx(ctx, &groups, cacheServerGroupAllKeys, func(conn *gorm.DB, v interface{}) error { - return conn.Find(&groups).Error - }) - return groups, err -} - -// BatchDeleteNodeGroup deletes multiple groups. -func (m *customServerModel) BatchDeleteNodeGroup(ctx context.Context, ids []int64) error { - return m.Transaction(ctx, func(tx *gorm.DB) error { - for _, id := range ids { - if err := m.Delete(ctx, id); err != nil { - return err - } - } - return nil - }) -} - -// InsertGroup inserts a group. -func (m *customServerModel) InsertGroup(ctx context.Context, data *Group) error { - return m.ExecCtx(ctx, func(conn *gorm.DB) error { - return conn.Create(data).Error - }, cacheServerGroupAllKeys) -} - -// FindOneGroup finds a group. -func (m *customServerModel) FindOneGroup(ctx context.Context, id int64) (*Group, error) { - var group Group - err := m.QueryCtx(ctx, &group, fmt.Sprintf("cache:serverGroup:%v", id), func(conn *gorm.DB, v interface{}) error { - return conn.Model(&Group{}).Where("id = ?", id).First(&group).Error - }) - return &group, err -} - -// UpdateGroup updates a group. -func (m *customServerModel) UpdateGroup(ctx context.Context, data *Group) error { - return m.ExecCtx(ctx, func(conn *gorm.DB) error { - return conn.Model(&Group{}).Where("id = ?", data.Id).Updates(data).Error - }, cacheServerGroupAllKeys, fmt.Sprintf("cache:serverGroup:%v", data.Id)) -} - -// DeleteGroup deletes a group. -func (m *customServerModel) DeleteGroup(ctx context.Context, id int64) error { - return m.ExecCtx(ctx, func(conn *gorm.DB) error { - return conn.Where("id = ?", id).Delete(&Group{}).Error - }, cacheServerGroupAllKeys, fmt.Sprintf("cache:serverGroup:%v", id)) -} - -// FindServerDetailByGroupIdsAndIds finds server details by group IDs and IDs. -func (m *customServerModel) FindServerDetailByGroupIdsAndIds(ctx context.Context, groupId, ids []int64) ([]*Server, error) { - if len(groupId) == 0 && len(ids) == 0 { - return []*Server{}, nil - } - var list []*Server - err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { - conn = conn. - Model(&Server{}). - Where("`enable` = ?", true) - if len(groupId) > 0 && len(ids) > 0 { - // OR is used to connect group_id and id conditions - conn = conn.Where("(`group_id` IN ? OR `id` IN ?)", groupId, ids) - } else if len(groupId) > 0 { - conn = conn.Where("`group_id` IN ?", groupId) - } else if len(ids) > 0 { - conn = conn.Where("`id` IN ?", ids) - } - - return conn.Order("sort ASC").Find(v).Error - }) - return list, err -} - -func (m *customServerModel) FindServerListByGroupIds(ctx context.Context, groupId []int64) ([]*Server, error) { - var data []*Server - err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error { - return conn.Model(&Server{}).Where("group_id IN ?", groupId).Find(v).Error - }) - return data, err -} - -func (m *customServerModel) FindAllServer(ctx context.Context) ([]*Server, error) { - var data []*Server - err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error { - return conn.Model(&Server{}).Order("sort ASC").Find(v).Error - }) - return data, err -} - -func (m *customServerModel) FindNodeByServerAddrAndProtocol(ctx context.Context, serverAddr string, protocol string) ([]*Server, error) { - var data []*Server - err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error { - return conn.Model(&Server{}).Where("server_addr = ? and protocol = ?", serverAddr, protocol).Order("sort ASC").Find(v).Error - }) - return data, err -} - -func (m *customServerModel) FindServerMinSortByIds(ctx context.Context, ids []int64) (int64, error) { - var minSort int64 - err := m.QueryNoCacheCtx(ctx, &minSort, func(conn *gorm.DB, v interface{}) error { - return conn.Model(&Server{}).Where("id IN ?", ids).Select("COALESCE(MIN(sort), 0)").Scan(v).Error - }) - return minSort, err -} - -func (m *customServerModel) FindServerListByIds(ctx context.Context, ids []int64) ([]*Server, error) { - var list []*Server - err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { - return conn.Model(&Server{}).Where("id IN ?", ids).Find(v).Error - }) - return list, err -} - -// InsertRuleGroup inserts a group. -func (m *customServerModel) InsertRuleGroup(ctx context.Context, data *RuleGroup) error { - return m.ExecCtx(ctx, func(conn *gorm.DB) error { - return conn.Where(&RuleGroup{}).Create(data).Error - }, cacheServerRuleGroupAllKeys, fmt.Sprintf("cache:serverRuleGroup:%v", data.Id)) -} - -// FindOneRuleGroup finds a group. -func (m *customServerModel) FindOneRuleGroup(ctx context.Context, id int64) (*RuleGroup, error) { - var group RuleGroup - err := m.QueryCtx(ctx, &group, fmt.Sprintf("cache:serverRuleGroup:%v", id), func(conn *gorm.DB, v interface{}) error { - return conn.Where(&RuleGroup{}).Model(&RuleGroup{}).Where("id = ?", id).First(&group).Error - }) - return &group, err -} - -// UpdateRuleGroup updates a group. -func (m *customServerModel) UpdateRuleGroup(ctx context.Context, data *RuleGroup) error { - return m.ExecCtx(ctx, func(conn *gorm.DB) error { - return conn.Where(&RuleGroup{}).Model(&RuleGroup{}).Where("id = ?", data.Id).Save(data).Error - }, cacheServerRuleGroupAllKeys, fmt.Sprintf("cache:serverRuleGroup:%v", data.Id)) -} - -// DeleteRuleGroup deletes a group. -func (m *customServerModel) DeleteRuleGroup(ctx context.Context, id int64) error { - return m.ExecCtx(ctx, func(conn *gorm.DB) error { - return conn.Where(&RuleGroup{}).Where("id = ?", id).Delete(&RuleGroup{}).Error - }, cacheServerRuleGroupAllKeys, fmt.Sprintf("cache:serverRuleGroup:%v", id)) -} - -// QueryAllRuleGroup returns all rule groups. -func (m *customServerModel) QueryAllRuleGroup(ctx context.Context) ([]*RuleGroup, error) { - var groups []*RuleGroup - err := m.QueryCtx(ctx, &groups, cacheServerRuleGroupAllKeys, func(conn *gorm.DB, v interface{}) error { - return conn.Where(&RuleGroup{}).Find(&groups).Error - }) - return groups, err -} - -func (m *customServerModel) FindServerListByFilter(ctx context.Context, filter *ServerFilter) (total int64, list []*Server, err error) { - var data []*Server - if filter == nil { - filter = &ServerFilter{ - Page: 1, - Size: 10, - } - } - - if filter.Page <= 0 { - filter.Page = 1 - } - if filter.Size <= 0 { - filter.Size = 10 - } - - err = m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error { - query := conn.Model(&Server{}).Order("sort ASC") - if filter.Group > 0 { - query = conn.Where("group_id = ?", filter.Group) - } - if filter.Search != "" { - query = query.Where("name LIKE ? OR server_addr LIKE ? OR tags LIKE ?", "%"+filter.Search+"%", "%"+filter.Search+"%", "%"+filter.Search+"%") - } - if len(filter.Tags) > 0 { - for i, tag := range filter.Tags { - if i == 0 { - query = query.Where("tags LIKE ?", "%"+tag+"%") - } else { - query = query.Or("tags LIKE ?", "%"+tag+"%") - } - } - } - return query.Count(&total).Limit(filter.Size).Offset((filter.Page - 1) * filter.Size).Find(v).Error - }) - if err != nil { - return 0, nil, err - } - return total, data, nil -} - -func (m *customServerModel) FindServerTags(ctx context.Context) ([]string, error) { - var data []string - err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error { - return conn.Model(&Server{}).Distinct("tags").Pluck("tags", v).Error - }) - var tags []string - for _, tag := range data { - if strings.Contains(tag, ",") { - tags = append(tags, strings.Split(tag, ",")...) - } else { - tags = append(tags, tag) - } - } - return tags, err -} - -func (m *customServerModel) FindServersByTag(ctx context.Context, tag string) ([]*Server, error) { - var data []*Server - err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error { - return conn.Model(&Server{}).Where("FIND_IN_SET(?, tags)", tag).Order("sort ASC").Find(v).Error - }) - return data, err -} - -// SetDefaultRuleGroup sets the default rule group. - -func (m *customServerModel) SetDefaultRuleGroup(ctx context.Context, id int64) error { - return m.ExecCtx(ctx, func(conn *gorm.DB) error { - // Reset all groups to not default - if err := conn.Model(&RuleGroup{}).Where("`id` != ?", id).Update("default", false).Error; err != nil { - return err - } - // Set the specified group as default - return conn.Model(&RuleGroup{}).Where("`id` = ?", id).Update("default", true).Error - }, cacheServerRuleGroupAllKeys, fmt.Sprintf("cache:serverRuleGroup:%v", id)) -} diff --git a/internal/model/server/server.go b/internal/model/server/server.go deleted file mode 100644 index 2da10da..0000000 --- a/internal/model/server/server.go +++ /dev/null @@ -1,219 +0,0 @@ -package server - -import ( - "time" - - "github.com/perfect-panel/server/pkg/logger" - - "gorm.io/gorm" -) - -const ( - RelayModeNone = "none" - RelayModeAll = "all" - RelayModeRandom = "random" - RuleGroupTypeReject = "reject" - RuleGroupTypeDefault = "default" - RuleGroupTypeDirect = "direct" -) - -type ServerFilter struct { - Id int64 - Tags []string - Group int64 - Search string - Page int - Size int -} - -// Deprecated: use internal/model/node/server.go -type Server struct { - Id int64 `gorm:"primary_key"` - Name string `gorm:"type:varchar(100);not null;default:'';comment:Node Name"` - Tags string `gorm:"type:varchar(128);not null;default:'';comment:Tags"` - Country string `gorm:"type:varchar(128);not null;default:'';comment:Country"` - City string `gorm:"type:varchar(128);not null;default:'';comment:City"` - Latitude string `gorm:"type:varchar(128);not null;default:'';comment:Latitude"` - Longitude string `gorm:"type:varchar(128);not null;default:'';comment:Longitude"` - ServerAddr string `gorm:"type:varchar(100);not null;default:'';comment:Server Address"` - RelayMode string `gorm:"type:varchar(20);not null;default:'none';comment:Relay Mode"` - RelayNode string `gorm:"type:text;comment:Relay Node"` - SpeedLimit int `gorm:"type:int;not null;default:0;comment:Speed Limit"` - TrafficRatio float32 `gorm:"type:DECIMAL(4,2);not null;default:0;comment:Traffic Ratio"` - GroupId int64 `gorm:"index:idx_group_id;type:int;default:null;comment:Group ID"` - Protocol string `gorm:"type:varchar(20);not null;default:'';comment:Protocol"` - Config string `gorm:"type:text;comment:Config"` - Enable *bool `gorm:"type:tinyint(1);not null;default:1;comment:Enabled"` - Sort int64 `gorm:"type:int;not null;default:0;comment:Sort"` - 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 { - return "server" -} - -func (s *Server) BeforeDelete(tx *gorm.DB) error { - logger.Debugf("[Server] BeforeDelete") - if err := tx.Exec("UPDATE `server` SET sort = sort - 1 WHERE sort > ?", s.Sort).Error; err != nil { - return err - } - return nil -} - -func (s *Server) BeforeUpdate(tx *gorm.DB) error { - logger.Debugf("[Server] BeforeUpdate") - var count int64 - if err := tx.Set("gorm:query_option", "FOR UPDATE").Model(&Server{}). - Where("sort = ? AND id != ?", s.Sort, s.Id).Count(&count).Error; err != nil { - return err - } - if count > 1 { - // reorder sort - if err := reorderSort(tx); err != nil { - logger.Errorf("[Server] BeforeUpdate reorderSort error: %v", err.Error()) - return err - } - // get max sort - var maxSort int64 - if err := tx.Model(&Server{}).Select("MAX(sort)").Scan(&maxSort).Error; err != nil { - return err - } - s.Sort = maxSort + 1 - } - return nil -} - -func (s *Server) BeforeCreate(tx *gorm.DB) error { - logger.Debugf("[Server] BeforeCreate") - if s.Sort == 0 { - var maxSort int64 - if err := tx.Model(&Server{}).Select("COALESCE(MAX(sort), 0)").Scan(&maxSort).Error; err != nil { - return err - } - s.Sort = maxSort + 1 - } - return nil -} - -type Vless struct { - Port int `json:"port"` - Flow string `json:"flow"` - Transport string `json:"transport"` - TransportConfig TransportConfig `json:"transport_config"` - Security string `json:"security"` - SecurityConfig SecurityConfig `json:"security_config"` -} - -type Vmess struct { - Port int `json:"port"` - Flow string `json:"flow"` - Transport string `json:"transport"` - TransportConfig TransportConfig `json:"transport_config"` - Security string `json:"security"` - SecurityConfig SecurityConfig `json:"security_config"` -} - -type Trojan struct { - Port int `json:"port"` - Flow string `json:"flow"` - Transport string `json:"transport"` - TransportConfig TransportConfig `json:"transport_config"` - Security string `json:"security"` - SecurityConfig SecurityConfig `json:"security_config"` -} - -type Shadowsocks struct { - Method string `json:"method"` - Port int `json:"port"` - ServerKey string `json:"server_key"` -} - -type Hysteria2 struct { - Port int `json:"port"` - HopPorts string `json:"hop_ports"` - HopInterval int `json:"hop_interval"` - ObfsPassword string `json:"obfs_password"` - SecurityConfig SecurityConfig `json:"security_config"` -} - -type Tuic struct { - Port int `json:"port"` - DisableSNI bool `json:"disable_sni"` - ReduceRtt bool `json:"reduce_rtt"` - UDPRelayMode string `json:"udp_relay_mode"` - CongestionController string `json:"congestion_controller"` - SecurityConfig SecurityConfig `json:"security_config"` -} - -type AnyTLS struct { - Port int `json:"port"` - SecurityConfig SecurityConfig `json:"security_config"` -} - -type TransportConfig struct { - Path string `json:"path,omitempty"` // ws/httpupgrade - Host string `json:"host,omitempty"` - ServiceName string `json:"service_name"` // grpc -} - -type SecurityConfig struct { - SNI string `json:"sni"` - AllowInsecure bool `json:"allow_insecure"` - Fingerprint string `json:"fingerprint"` - RealityServerAddr 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"` -} - -type NodeRelay struct { - Host string `json:"host"` - Port int `json:"port"` - Prefix string `json:"prefix"` -} - -type Group struct { - Id int64 `gorm:"primary_key"` - Name string `gorm:"type:varchar(100);not null;default:'';comment:Group Name"` - Description string `gorm:"type:varchar(255);default:'';comment:Group Description"` - CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` - UpdatedAt time.Time `gorm:"comment:Update Time"` -} - -func (Group) TableName() string { - return "server_group" -} - -type RuleGroup struct { - Id int64 `gorm:"primary_key"` - Icon string `gorm:"type:MEDIUMTEXT;comment:Rule Group Icon"` - Name string `gorm:"type:varchar(100);not null;default:'';comment:Rule Group Name"` - Type string `gorm:"type:varchar(100);not null;default:'';comment:Rule Group Type"` - Tags string `gorm:"type:text;comment:Selected Node Tags"` - Rules string `gorm:"type:MEDIUMTEXT;comment:Rules"` - Enable bool `gorm:"type:tinyint(1);not null;default:1;comment:Rule Group Enable"` - Default bool `gorm:"type:tinyint(1);not null;default:0;comment:Rule Group is Default"` - CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` - UpdatedAt time.Time `gorm:"comment:Update Time"` -} - -func (RuleGroup) TableName() string { - return "server_rule_group" -} -func reorderSort(tx *gorm.DB) error { - var servers []Server - if err := tx.Order("sort, id").Find(&servers).Error; err != nil { - return err - } - for i, server := range servers { - if server.Sort != int64(i)+1 { - if err := tx.Exec("UPDATE `server` SET sort = ? WHERE id = ?", i+1, server.Id).Error; err != nil { - return err - } - } - } - return nil -} diff --git a/internal/types/types.go b/internal/types/types.go index 565c4ce..e6353e9 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -1557,7 +1557,7 @@ type PurchaseOrderResponse struct { type QueryAnnouncementRequest struct { Page int `form:"page"` - Size int `form:"size,default=15"` + Size int `form:"size"` Pinned *bool `form:"pinned"` Popup *bool `form:"popup"` } From e027cbb5de8bb30ce928cfab49ba856e17a5c3e6 Mon Sep 17 00:00:00 2001 From: Chang lue Tsen Date: Tue, 23 Dec 2025 07:52:38 -0500 Subject: [PATCH 27/50] refactor(server): remove server table --- .../migrate/database/02122_server.down.sql | 27 +++++++++++++++++++ .../migrate/database/02122_server.up.sql | 1 + 2 files changed, 28 insertions(+) create mode 100644 initialize/migrate/database/02122_server.down.sql create mode 100644 initialize/migrate/database/02122_server.up.sql diff --git a/initialize/migrate/database/02122_server.down.sql b/initialize/migrate/database/02122_server.down.sql new file mode 100644 index 0000000..7053859 --- /dev/null +++ b/initialize/migrate/database/02122_server.down.sql @@ -0,0 +1,27 @@ +CREATE TABLE IF NOT EXISTS `server` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Node Name', + `tags` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Tags', + `country` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Country', + `city` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'City', + `latitude` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'latitude', + `longitude` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'longitude', + `server_addr` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Server Address', + `relay_mode` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'none' COMMENT 'Relay Mode', + `relay_node` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Relay Node', + `speed_limit` bigint NOT NULL DEFAULT '0' COMMENT 'Speed Limit', + `traffic_ratio` decimal(4, 2) NOT NULL DEFAULT '0.00' COMMENT 'Traffic Ratio', + `group_id` bigint DEFAULT NULL COMMENT 'Group ID', + `protocol` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Protocol', + `config` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Config', + `enable` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Enabled', + `sort` bigint NOT NULL DEFAULT '0' COMMENT 'Sort', + `last_reported_at` datetime(3) DEFAULT NULL COMMENT 'Last Reported Time', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`), + KEY `idx_group_id` (`group_id`) + ) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; diff --git a/initialize/migrate/database/02122_server.up.sql b/initialize/migrate/database/02122_server.up.sql new file mode 100644 index 0000000..2e506e1 --- /dev/null +++ b/initialize/migrate/database/02122_server.up.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS `server`; \ No newline at end of file From a9c832cb7c09e0cccf8cbf51bab4047ae3bcd2c3 Mon Sep 17 00:00:00 2001 From: Chang lue Tsen Date: Sat, 27 Dec 2025 10:45:28 -0500 Subject: [PATCH 28/50] feat(user): implement soft deletion for user accounts and update related logic --- apis/types.api | 1 - internal/logic/auth/userLoginLogic.go | 4 ++ internal/logic/auth/userRegisterLogic.go | 7 +++- internal/model/user/default.go | 25 ++++-------- internal/model/user/user.go | 49 +++++++++++++----------- internal/types/types.go | 1 - 6 files changed, 42 insertions(+), 45 deletions(-) diff --git a/apis/types.api b/apis/types.api index 8b4a9aa..85ecf63 100644 --- a/apis/types.api +++ b/apis/types.api @@ -32,7 +32,6 @@ type ( CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` DeletedAt int64 `json:"deleted_at,omitempty"` - IsDel bool `json:"is_del,omitempty"` } Follow { Id int64 `json:"id"` diff --git a/internal/logic/auth/userLoginLogic.go b/internal/logic/auth/userLoginLogic.go index deecaae..bc3c2fa 100644 --- a/internal/logic/auth/userLoginLogic.go +++ b/internal/logic/auth/userLoginLogic.go @@ -68,6 +68,10 @@ func (l *UserLoginLogic) UserLogin(req *types.UserLoginRequest) (resp *types.Log userInfo, err = l.svcCtx.UserModel.FindOneByEmail(l.ctx, req.Email) + if userInfo.DeletedAt.Valid { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "user email deleted: %v", req.Email) + } + if err != nil { if errors.As(err, &gorm.ErrRecordNotFound) { return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "user email not exist: %v", req.Email) diff --git a/internal/logic/auth/userRegisterLogic.go b/internal/logic/auth/userRegisterLogic.go index cf959a9..8b622c0 100644 --- a/internal/logic/auth/userRegisterLogic.go +++ b/internal/logic/auth/userRegisterLogic.go @@ -79,13 +79,16 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp * } } // Check if the user exists - _, err = l.svcCtx.UserModel.FindOneByEmail(l.ctx, req.Email) + u, err := l.svcCtx.UserModel.FindOneByEmail(l.ctx, req.Email) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { l.Errorw("FindOneByEmail Error", logger.Field("error", err)) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user info failed: %v", err.Error()) - } else if err == nil { + } else if err == nil && !u.DeletedAt.Valid { return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserExist), "user email exist: %v", req.Email) + } else if err == nil && u.DeletedAt.Valid { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserDisabled), "user email deleted: %v", req.Email) } + // Generate password pwd := tool.EncodePassWord(req.Password) userInfo := &user.User{ diff --git a/internal/model/user/default.go b/internal/model/user/default.go index e2a326a..8eeb399 100644 --- a/internal/model/user/default.go +++ b/internal/model/user/default.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/perfect-panel/server/pkg/cache" + "github.com/perfect-panel/server/pkg/logger" "github.com/redis/go-redis/v9" "gorm.io/gorm" ) @@ -72,7 +73,7 @@ func (m *defaultUserModel) FindOneByEmail(ctx context.Context, email string) (*U if err := conn.Model(&AuthMethods{}).Where("`auth_type` = 'email' AND `auth_identifier` = ?", email).First(&data).Error; err != nil { return err } - return conn.Model(&User{}).Where("`id` = ?", data.UserId).Preload("UserDevices").Preload("AuthMethods").First(v).Error + return conn.Model(&User{}).Unscoped().Where("`id` = ?", data.UserId).Preload("UserDevices").Preload("AuthMethods").First(v).Error }) return &user, err } @@ -91,7 +92,7 @@ func (m *defaultUserModel) FindOne(ctx context.Context, id int64) (*User, error) userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, id) var resp User err := m.QueryCtx(ctx, &resp, userIdKey, func(conn *gorm.DB, v interface{}) error { - return conn.Model(&User{}).Where("`id` = ?", id).Preload("UserDevices").Preload("AuthMethods").First(&resp).Error + return conn.Model(&User{}).Unscoped().Where("`id` = ?", id).Preload("UserDevices").Preload("AuthMethods").First(&resp).Error }) return &resp, err } @@ -119,10 +120,11 @@ func (m *defaultUserModel) Delete(ctx context.Context, id int64, tx ...*gorm.DB) return err } - // 使用批量相关缓存清理,包含所有相关数据的缓存 + // Use batch related cache cleaning, including a cache of all relevant data defer func() { if clearErr := m.BatchClearRelatedCache(ctx, data); clearErr != nil { - // 记录清理缓存错误,但不阻断删除操作 + // Record cache cleaning errors, but do not block deletion operations + logger.Errorf("failed to clear related cache for user %d: %v", id, clearErr.Error()) } }() @@ -130,24 +132,11 @@ func (m *defaultUserModel) Delete(ctx context.Context, id int64, tx ...*gorm.DB) if len(tx) > 0 { db = tx[0] } - - // 删除用户相关的所有数据 + // Soft deletion of user information without any processing of other information (Determine whether to allow login/subscription based on the user's deletion status) if err := db.Model(&User{}).Where("`id` = ?", id).Delete(&User{}).Error; err != nil { return err } - if err := db.Model(&AuthMethods{}).Where("`user_id` = ?", id).Delete(&AuthMethods{}).Error; err != nil { - return err - } - - if err := db.Model(&Subscribe{}).Where("`user_id` = ?", id).Delete(&Subscribe{}).Error; err != nil { - return err - } - - if err := db.Model(&Device{}).Where("`user_id` = ?", id).Delete(&Device{}).Error; err != nil { - return err - } - return nil }) } diff --git a/internal/model/user/user.go b/internal/model/user/user.go index 923594e..80db348 100644 --- a/internal/model/user/user.go +++ b/internal/model/user/user.go @@ -2,32 +2,35 @@ package user import ( "time" + + "gorm.io/gorm" ) type User struct { - Id int64 `gorm:"primaryKey"` - Password string `gorm:"type:varchar(100);not null;comment:User Password"` - Algo string `gorm:"type:varchar(20);default:'default';comment:Encryption Algorithm"` - Salt string `gorm:"type:varchar(20);default:null;comment:Password Salt"` - Avatar string `gorm:"type:MEDIUMTEXT;comment:User Avatar"` - Balance int64 `gorm:"default:0;comment:User Balance"` // User Balance Amount - ReferCode string `gorm:"type:varchar(20);default:'';comment:Referral Code"` - RefererId int64 `gorm:"index:idx_referer;comment:Referrer ID"` - Commission int64 `gorm:"default:0;comment:Commission"` // Commission Amount - ReferralPercentage uint8 `gorm:"default:0;comment:Referral"` // Referral Percentage - OnlyFirstPurchase *bool `gorm:"default:true;not null;comment:Only First Purchase"` // Only First Purchase Referral - GiftAmount int64 `gorm:"default:0;comment:User Gift Amount"` - Enable *bool `gorm:"default:true;not null;comment:Is Account Enabled"` - IsAdmin *bool `gorm:"default:false;not null;comment:Is Admin"` - EnableBalanceNotify *bool `gorm:"default:false;not null;comment:Enable Balance Change Notifications"` - EnableLoginNotify *bool `gorm:"default:false;not null;comment:Enable Login Notifications"` - EnableSubscribeNotify *bool `gorm:"default:false;not null;comment:Enable Subscription Notifications"` - EnableTradeNotify *bool `gorm:"default:false;not null;comment:Enable Trade Notifications"` - AuthMethods []AuthMethods `gorm:"foreignKey:UserId;references:Id"` - UserDevices []Device `gorm:"foreignKey:UserId;references:Id"` - Rules string `gorm:"type:TEXT;comment:User Rules"` - CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` - UpdatedAt time.Time `gorm:"comment:Update Time"` + Id int64 `gorm:"primaryKey"` + Password string `gorm:"type:varchar(100);not null;comment:User Password"` + Algo string `gorm:"type:varchar(20);default:'default';comment:Encryption Algorithm"` + Salt string `gorm:"type:varchar(20);default:null;comment:Password Salt"` + Avatar string `gorm:"type:MEDIUMTEXT;comment:User Avatar"` + Balance int64 `gorm:"default:0;comment:User Balance"` // User Balance Amount + ReferCode string `gorm:"type:varchar(20);default:'';comment:Referral Code"` + RefererId int64 `gorm:"index:idx_referer;comment:Referrer ID"` + Commission int64 `gorm:"default:0;comment:Commission"` // Commission Amount + ReferralPercentage uint8 `gorm:"default:0;comment:Referral"` // Referral Percentage + OnlyFirstPurchase *bool `gorm:"default:true;not null;comment:Only First Purchase"` // Only First Purchase Referral + GiftAmount int64 `gorm:"default:0;comment:User Gift Amount"` + Enable *bool `gorm:"default:true;not null;comment:Is Account Enabled"` + IsAdmin *bool `gorm:"default:false;not null;comment:Is Admin"` + EnableBalanceNotify *bool `gorm:"default:false;not null;comment:Enable Balance Change Notifications"` + EnableLoginNotify *bool `gorm:"default:false;not null;comment:Enable Login Notifications"` + EnableSubscribeNotify *bool `gorm:"default:false;not null;comment:Enable Subscription Notifications"` + EnableTradeNotify *bool `gorm:"default:false;not null;comment:Enable Trade Notifications"` + AuthMethods []AuthMethods `gorm:"foreignKey:UserId;references:Id"` + UserDevices []Device `gorm:"foreignKey:UserId;references:Id"` + Rules string `gorm:"type:TEXT;comment:User Rules"` + CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` + DeletedAt gorm.DeletedAt `gorm:"index;comment:Deletion Time"` } func (*User) TableName() string { diff --git a/internal/types/types.go b/internal/types/types.go index e6353e9..c7bf4bf 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -2540,7 +2540,6 @@ type User struct { CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` DeletedAt int64 `json:"deleted_at,omitempty"` - IsDel bool `json:"is_del,omitempty"` } type UserAffiliate struct { From 67f16ead82f22c2b883345db8bca3532f359a5cd Mon Sep 17 00:00:00 2001 From: Chang lue Tsen Date: Sat, 27 Dec 2025 10:57:34 -0500 Subject: [PATCH 29/50] feat(user): add unscoped filter to include soft-deleted records in user queries --- apis/admin/user.api | 1 + internal/logic/admin/user/getUserListLogic.go | 1 + internal/model/user/model.go | 4 ++++ internal/types/types.go | 1 + 4 files changed, 7 insertions(+) diff --git a/apis/admin/user.api b/apis/admin/user.api index dc0c5e8..699fe6d 100644 --- a/apis/admin/user.api +++ b/apis/admin/user.api @@ -19,6 +19,7 @@ type ( Size int `form:"size"` Search string `form:"search,omitempty"` UserId *int64 `form:"user_id,omitempty"` + Unscoped bool `form:"unscoped,omitempty"` SubscribeId *int64 `form:"subscribe_id,omitempty"` UserSubscribeId *int64 `form:"user_subscribe_id,omitempty"` } diff --git a/internal/logic/admin/user/getUserListLogic.go b/internal/logic/admin/user/getUserListLogic.go index 3859f76..8935af1 100644 --- a/internal/logic/admin/user/getUserListLogic.go +++ b/internal/logic/admin/user/getUserListLogic.go @@ -30,6 +30,7 @@ func (l *GetUserListLogic) GetUserList(req *types.GetUserListRequest) (*types.Ge list, total, err := l.svcCtx.UserModel.QueryPageList(l.ctx, req.Page, req.Size, &user.UserFilterParams{ UserId: req.UserId, Search: req.Search, + Unscoped: req.Unscoped, SubscribeId: req.SubscribeId, UserSubscribeId: req.UserSubscribeId, Order: "DESC", diff --git a/internal/model/user/model.go b/internal/model/user/model.go index 86caa0d..65b3589 100644 --- a/internal/model/user/model.go +++ b/internal/model/user/model.go @@ -62,6 +62,7 @@ type UserFilterParams struct { SubscribeId *int64 UserSubscribeId *int64 Order string // Order by id, e.g., "desc" + Unscoped bool // Whether to include soft-deleted records } type customUserLogicModel interface { @@ -148,6 +149,9 @@ func (m *customUserModel) QueryPageList(ctx context.Context, page, size int, fil if filter.Order != "" { conn = conn.Order(fmt.Sprintf("user.id %s", filter.Order)) } + if filter.Unscoped { + conn = conn.Unscoped() + } } return conn.Model(&User{}).Group("user.id").Count(&total).Limit(size).Offset((page - 1) * size).Preload("UserDevices").Preload("AuthMethods").Find(&list).Error }) diff --git a/internal/types/types.go b/internal/types/types.go index c7bf4bf..d44d465 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -1043,6 +1043,7 @@ type GetUserListRequest struct { Size int `form:"size"` Search string `form:"search,omitempty"` UserId *int64 `form:"user_id,omitempty"` + Unscoped bool `form:"unscoped,omitempty"` SubscribeId *int64 `form:"subscribe_id,omitempty"` UserSubscribeId *int64 `form:"user_subscribe_id,omitempty"` } From 31e634ba662080a78bbd695729279bf8e0b68c9f Mon Sep 17 00:00:00 2001 From: Tension Date: Sun, 28 Dec 2025 16:49:28 +0800 Subject: [PATCH 30/50] feat(user): add handlers and logic for resetting user subscribe token and traffic --- apis/admin/user.api | 18 ++++++ apis/public/user.api | 4 +- apis/types.api | 4 ++ .../user/resetUserSubscribeTokenHandler.go | 26 +++++++++ .../user/resetUserSubscribeTrafficHandler.go | 26 +++++++++ .../admin/user/stopUserSubscribeHandler.go | 26 +++++++++ internal/handler/routes.go | 9 +++ .../user/resetUserSubscribeTokenLogic.go | 55 +++++++++++++++++++ .../user/resetUserSubscribeTrafficLogic.go | 53 ++++++++++++++++++ .../admin/user/stopUserSubscribeLogic.go | 53 ++++++++++++++++++ internal/model/user/user.go | 2 +- internal/types/types.go | 8 +++ 12 files changed, 280 insertions(+), 4 deletions(-) create mode 100644 internal/handler/admin/user/resetUserSubscribeTokenHandler.go create mode 100644 internal/handler/admin/user/resetUserSubscribeTrafficHandler.go create mode 100644 internal/handler/admin/user/stopUserSubscribeHandler.go create mode 100644 internal/logic/admin/user/resetUserSubscribeTokenLogic.go create mode 100644 internal/logic/admin/user/resetUserSubscribeTrafficLogic.go create mode 100644 internal/logic/admin/user/stopUserSubscribeLogic.go diff --git a/apis/admin/user.api b/apis/admin/user.api index 699fe6d..1f01a6f 100644 --- a/apis/admin/user.api +++ b/apis/admin/user.api @@ -184,6 +184,12 @@ type ( GetUserSubscribeByIdRequest { Id int64 `form:"id" validate:"required"` } + StopUserSubscribeRequest { + UserSubscribeId int64 `json:"user_subscribe_id"` + } + ResetUserSubscribeTrafficRequest { + UserSubscribeId int64 `json:"user_subscribe_id"` + } ) @server ( @@ -292,5 +298,17 @@ service ppanel { @doc "Get user login logs" @handler GetUserLoginLogs get /login/logs (GetUserLoginLogsRequest) returns (GetUserLoginLogsResponse) + + @doc "Reset user subscribe token" + @handler ResetUserSubscribeToken + post /subscribe/reset/token (ResetUserSubscribeTokenRequest) + + @doc "Stop user subscribe" + @handler StopUserSubscribe + post /subscribe/stop (StopUserSubscribeRequest) + + @doc "Reset user subscribe traffic" + @handler ResetUserSubscribeTraffic + post /subscribe/reset/traffic (ResetUserSubscribeTrafficRequest) } diff --git a/apis/public/user.api b/apis/public/user.api index f37eef3..3612328 100644 --- a/apis/public/user.api +++ b/apis/public/user.api @@ -66,9 +66,7 @@ type ( UnbindOAuthRequest { Method string `json:"method"` } - ResetUserSubscribeTokenRequest { - UserSubscribeId int64 `json:"user_subscribe_id"` - } + GetLoginLogRequest { Page int `form:"page"` Size int `form:"size"` diff --git a/apis/types.api b/apis/types.api index 85ecf63..b212734 100644 --- a/apis/types.api +++ b/apis/types.api @@ -844,5 +844,9 @@ type ( CertDNSProvider string `json:"cert_dns_provider,omitempty"` // DNS provider for certificate CertDNSEnv string `json:"cert_dns_env,omitempty"` // Environment for DNS provider } + // reset user subscribe token + ResetUserSubscribeTokenRequest { + UserSubscribeId int64 `json:"user_subscribe_id"` + } ) diff --git a/internal/handler/admin/user/resetUserSubscribeTokenHandler.go b/internal/handler/admin/user/resetUserSubscribeTokenHandler.go new file mode 100644 index 0000000..0ccff0c --- /dev/null +++ b/internal/handler/admin/user/resetUserSubscribeTokenHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Reset user subscribe token +func ResetUserSubscribeTokenHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.ResetUserSubscribeTokenRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewResetUserSubscribeTokenLogic(c.Request.Context(), svcCtx) + err := l.ResetUserSubscribeToken(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/user/resetUserSubscribeTrafficHandler.go b/internal/handler/admin/user/resetUserSubscribeTrafficHandler.go new file mode 100644 index 0000000..80dccfc --- /dev/null +++ b/internal/handler/admin/user/resetUserSubscribeTrafficHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Reset user subscribe traffic +func ResetUserSubscribeTrafficHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.ResetUserSubscribeTrafficRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewResetUserSubscribeTrafficLogic(c.Request.Context(), svcCtx) + err := l.ResetUserSubscribeTraffic(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/user/stopUserSubscribeHandler.go b/internal/handler/admin/user/stopUserSubscribeHandler.go new file mode 100644 index 0000000..d61f04a --- /dev/null +++ b/internal/handler/admin/user/stopUserSubscribeHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Stop user subscribe +func StopUserSubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.StopUserSubscribeRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewStopUserSubscribeLogic(c.Request.Context(), svcCtx) + err := l.StopUserSubscribe(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/routes.go b/internal/handler/routes.go index 2ac18a8..2d41010 100644 --- a/internal/handler/routes.go +++ b/internal/handler/routes.go @@ -576,6 +576,15 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Get user subcribe reset traffic logs adminUserGroupRouter.GET("/subscribe/reset/logs", adminUser.GetUserSubscribeResetTrafficLogsHandler(serverCtx)) + // Reset user subscribe token + adminUserGroupRouter.POST("/subscribe/reset/token", adminUser.ResetUserSubscribeTokenHandler(serverCtx)) + + // Reset user subscribe traffic + adminUserGroupRouter.POST("/subscribe/reset/traffic", adminUser.ResetUserSubscribeTrafficHandler(serverCtx)) + + // Stop user subscribe + adminUserGroupRouter.POST("/subscribe/stop", adminUser.StopUserSubscribeHandler(serverCtx)) + // Get user subcribe traffic logs adminUserGroupRouter.GET("/subscribe/traffic_logs", adminUser.GetUserSubscribeTrafficLogsHandler(serverCtx)) } diff --git a/internal/logic/admin/user/resetUserSubscribeTokenLogic.go b/internal/logic/admin/user/resetUserSubscribeTokenLogic.go new file mode 100644 index 0000000..02f46ca --- /dev/null +++ b/internal/logic/admin/user/resetUserSubscribeTokenLogic.go @@ -0,0 +1,55 @@ +package user + +import ( + "context" + "fmt" + "time" + + "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/uuidx" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type ResetUserSubscribeTokenLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewResetUserSubscribeTokenLogic Reset user subscribe token +func NewResetUserSubscribeTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ResetUserSubscribeTokenLogic { + return &ResetUserSubscribeTokenLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *ResetUserSubscribeTokenLogic) ResetUserSubscribeToken(req *types.ResetUserSubscribeTokenRequest) error { + userSub, err := l.svcCtx.UserModel.FindOneSubscribe(l.ctx, req.UserSubscribeId) + if err != nil { + logger.Errorf("[ResetUserSubscribeToken] FindOneSubscribe error: %v", err.Error()) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindOneSubscribe error: %v", err.Error()) + } + userSub.Token = uuidx.SubscribeToken(fmt.Sprintf("AdminUpdate:%d", time.Now().UnixMilli())) + + err = l.svcCtx.UserModel.UpdateSubscribe(l.ctx, userSub) + if err != nil { + logger.Errorf("[ResetUserSubscribeToken] UpdateSubscribe error: %v", err.Error()) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "UpdateSubscribe error: %v", err.Error()) + } + // Clear user subscribe cache + if err = l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, userSub); err != nil { + l.Errorw("ClearSubscribeCache failed:", logger.Field("error", err.Error()), logger.Field("userSubscribeId", userSub.Id)) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "ClearSubscribeCache failed: %v", err.Error()) + } + // Clear subscribe cache + if err = l.svcCtx.SubscribeModel.ClearCache(l.ctx, userSub.SubscribeId); err != nil { + l.Errorw("failed to clear subscribe cache", logger.Field("error", err.Error()), logger.Field("subscribeId", userSub.SubscribeId)) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "failed to clear subscribe cache: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/admin/user/resetUserSubscribeTrafficLogic.go b/internal/logic/admin/user/resetUserSubscribeTrafficLogic.go new file mode 100644 index 0000000..5b90147 --- /dev/null +++ b/internal/logic/admin/user/resetUserSubscribeTrafficLogic.go @@ -0,0 +1,53 @@ +package user + +import ( + "context" + + "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 ResetUserSubscribeTrafficLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewResetUserSubscribeTrafficLogic Reset user subscribe traffic +func NewResetUserSubscribeTrafficLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ResetUserSubscribeTrafficLogic { + return &ResetUserSubscribeTrafficLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *ResetUserSubscribeTrafficLogic) ResetUserSubscribeTraffic(req *types.ResetUserSubscribeTrafficRequest) error { + userSub, err := l.svcCtx.UserModel.FindOneSubscribe(l.ctx, req.UserSubscribeId) + if err != nil { + l.Errorw("FindOneSubscribe error", logger.Field("error", err.Error()), logger.Field("userSubscribeId", req.UserSubscribeId)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), " FindOneSubscribe error: %v", err.Error()) + } + userSub.Download = 0 + userSub.Upload = 0 + + err = l.svcCtx.UserModel.UpdateSubscribe(l.ctx, userSub) + if err != nil { + l.Errorw("UpdateSubscribe error", logger.Field("error", err.Error()), logger.Field("userSubscribeId", req.UserSubscribeId)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), " UpdateSubscribe error: %v", err.Error()) + } + // Clear user subscribe cache + if err = l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, userSub); err != nil { + l.Errorw("ClearSubscribeCache failed:", logger.Field("error", err.Error()), logger.Field("userSubscribeId", userSub.Id)) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "ClearSubscribeCache failed: %v", err.Error()) + } + // Clear subscribe cache + if err = l.svcCtx.SubscribeModel.ClearCache(l.ctx, userSub.SubscribeId); err != nil { + l.Errorw("failed to clear subscribe cache", logger.Field("error", err.Error()), logger.Field("subscribeId", userSub.SubscribeId)) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "failed to clear subscribe cache: %v", err.Error()) + } + return nil +} diff --git a/internal/logic/admin/user/stopUserSubscribeLogic.go b/internal/logic/admin/user/stopUserSubscribeLogic.go new file mode 100644 index 0000000..658e4cf --- /dev/null +++ b/internal/logic/admin/user/stopUserSubscribeLogic.go @@ -0,0 +1,53 @@ +package user + +import ( + "context" + + "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 StopUserSubscribeLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewStopUserSubscribeLogic Stop user subscribe +func NewStopUserSubscribeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *StopUserSubscribeLogic { + return &StopUserSubscribeLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *StopUserSubscribeLogic) StopUserSubscribe(req *types.StopUserSubscribeRequest) error { + userSub, err := l.svcCtx.UserModel.FindOneSubscribe(l.ctx, req.UserSubscribeId) + if err != nil { + l.Errorw("FindOneSubscribe error", logger.Field("error", err.Error()), logger.Field("userSubscribeId", req.UserSubscribeId)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), " FindOneSubscribe error: %v", err.Error()) + } + userSub.Status = 5 // set status to stopped + err = l.svcCtx.UserModel.UpdateSubscribe(l.ctx, userSub) + if err != nil { + l.Errorw("UpdateSubscribe error", logger.Field("error", err.Error()), logger.Field("userSubscribeId", req.UserSubscribeId)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), " UpdateSubscribe error: %v", err.Error()) + } + + // Clear user subscribe cache + if err = l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, userSub); err != nil { + l.Errorw("ClearSubscribeCache failed:", logger.Field("error", err.Error()), logger.Field("userSubscribeId", userSub.Id)) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "ClearSubscribeCache failed: %v", err.Error()) + } + // Clear subscribe cache + if err = l.svcCtx.SubscribeModel.ClearCache(l.ctx, userSub.SubscribeId); err != nil { + l.Errorw("failed to clear subscribe cache", logger.Field("error", err.Error()), logger.Field("subscribeId", userSub.SubscribeId)) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "failed to clear subscribe cache: %v", err.Error()) + } + + return nil +} diff --git a/internal/model/user/user.go b/internal/model/user/user.go index 80db348..30fa804 100644 --- a/internal/model/user/user.go +++ b/internal/model/user/user.go @@ -51,7 +51,7 @@ type Subscribe struct { Upload int64 `gorm:"default:0;comment:Upload Traffic"` Token string `gorm:"index:idx_token;unique;type:varchar(255);default:'';comment:Token"` UUID string `gorm:"type:varchar(255);unique;index:idx_uuid;default:'';comment:UUID"` - Status uint8 `gorm:"type:tinyint(1);default:0;comment:Subscription Status: 0: Pending 1: Active 2: Finished 3: Expired 4: Deducted"` + Status uint8 `gorm:"type:tinyint(1);default:0;comment:Subscription Status: 0: Pending 1: Active 2: Finished 3: Expired 4: Deducted 5: stopped"` Note string `gorm:"type:varchar(500);default:'';comment:User note for subscription"` CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` UpdatedAt time.Time `gorm:"comment:Update Time"` diff --git a/internal/types/types.go b/internal/types/types.go index d44d465..44180f4 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -1846,6 +1846,10 @@ type ResetUserSubscribeTokenRequest struct { UserSubscribeId int64 `json:"user_subscribe_id"` } +type ResetUserSubscribeTrafficRequest struct { + UserSubscribeId int64 `json:"user_subscribe_id"` +} + type RevenueStatisticsResponse struct { Today OrdersStatistics `json:"today"` Monthly OrdersStatistics `json:"monthly"` @@ -2039,6 +2043,10 @@ type StopBatchSendEmailTaskRequest struct { Id int64 `json:"id"` } +type StopUserSubscribeRequest struct { + UserSubscribeId int64 `json:"user_subscribe_id"` +} + type StripePayment struct { Method string `json:"method"` ClientSecret string `json:"client_secret"` From d332e760f8efc264c75978d0a103dd7a7445dd27 Mon Sep 17 00:00:00 2001 From: Tension Date: Sun, 28 Dec 2025 17:08:26 +0800 Subject: [PATCH 31/50] feat(subscribe): add ShowOriginalPrice field and related database changes --- apis/admin/subscribe.api | 86 +++++------ apis/public/user.api | 1 - apis/types.api | 49 +++---- .../02123_subscribe_original.down.sql | 2 + .../database/02123_subscribe_original.up.sql | 2 + .../admin/subscribe/createSubscribeLogic.go | 45 +++--- .../admin/subscribe/updateSubscribeLogic.go | 45 +++--- internal/model/subscribe/subscribe.go | 49 +++---- internal/types/types.go | 135 +++++++++--------- 9 files changed, 213 insertions(+), 201 deletions(-) create mode 100644 initialize/migrate/database/02123_subscribe_original.down.sql create mode 100644 initialize/migrate/database/02123_subscribe_original.up.sql diff --git a/apis/admin/subscribe.api b/apis/admin/subscribe.api index bea205d..a832b3a 100644 --- a/apis/admin/subscribe.api +++ b/apis/admin/subscribe.api @@ -34,50 +34,52 @@ type ( Ids []int64 `json:"ids" validate:"required"` } CreateSubscribeRequest { - Name string `json:"name" validate:"required"` - Language string `json:"language"` - Description string `json:"description"` - UnitPrice int64 `json:"unit_price"` - UnitTime string `json:"unit_time"` - Discount []SubscribeDiscount `json:"discount"` - Replacement int64 `json:"replacement"` - Inventory int64 `json:"inventory"` - Traffic int64 `json:"traffic"` - SpeedLimit int64 `json:"speed_limit"` - DeviceLimit int64 `json:"device_limit"` - Quota int64 `json:"quota"` - Nodes []int64 `json:"nodes"` - NodeTags []string `json:"node_tags"` - Show *bool `json:"show"` - Sell *bool `json:"sell"` - DeductionRatio int64 `json:"deduction_ratio"` - AllowDeduction *bool `json:"allow_deduction"` - ResetCycle int64 `json:"reset_cycle"` - RenewalReset *bool `json:"renewal_reset"` + Name string `json:"name" validate:"required"` + Language string `json:"language"` + Description string `json:"description"` + UnitPrice int64 `json:"unit_price"` + UnitTime string `json:"unit_time"` + Discount []SubscribeDiscount `json:"discount"` + Replacement int64 `json:"replacement"` + Inventory int64 `json:"inventory"` + Traffic int64 `json:"traffic"` + SpeedLimit int64 `json:"speed_limit"` + DeviceLimit int64 `json:"device_limit"` + Quota int64 `json:"quota"` + Nodes []int64 `json:"nodes"` + NodeTags []string `json:"node_tags"` + Show *bool `json:"show"` + Sell *bool `json:"sell"` + DeductionRatio int64 `json:"deduction_ratio"` + AllowDeduction *bool `json:"allow_deduction"` + ResetCycle int64 `json:"reset_cycle"` + RenewalReset *bool `json:"renewal_reset"` + ShowOriginalPrice bool `json:"show_original_price"` } UpdateSubscribeRequest { - Id int64 `json:"id" validate:"required"` - Name string `json:"name" validate:"required"` - Language string `json:"language"` - Description string `json:"description"` - UnitPrice int64 `json:"unit_price"` - UnitTime string `json:"unit_time"` - Discount []SubscribeDiscount `json:"discount"` - Replacement int64 `json:"replacement"` - Inventory int64 `json:"inventory"` - Traffic int64 `json:"traffic"` - SpeedLimit int64 `json:"speed_limit"` - DeviceLimit int64 `json:"device_limit"` - Quota int64 `json:"quota"` - Nodes []int64 `json:"nodes"` - NodeTags []string `json:"node_tags"` - Show *bool `json:"show"` - Sell *bool `json:"sell"` - Sort int64 `json:"sort"` - DeductionRatio int64 `json:"deduction_ratio"` - AllowDeduction *bool `json:"allow_deduction"` - ResetCycle int64 `json:"reset_cycle"` - RenewalReset *bool `json:"renewal_reset"` + Id int64 `json:"id" validate:"required"` + Name string `json:"name" validate:"required"` + Language string `json:"language"` + Description string `json:"description"` + UnitPrice int64 `json:"unit_price"` + UnitTime string `json:"unit_time"` + Discount []SubscribeDiscount `json:"discount"` + Replacement int64 `json:"replacement"` + Inventory int64 `json:"inventory"` + Traffic int64 `json:"traffic"` + SpeedLimit int64 `json:"speed_limit"` + DeviceLimit int64 `json:"device_limit"` + Quota int64 `json:"quota"` + Nodes []int64 `json:"nodes"` + NodeTags []string `json:"node_tags"` + Show *bool `json:"show"` + Sell *bool `json:"sell"` + Sort int64 `json:"sort"` + DeductionRatio int64 `json:"deduction_ratio"` + AllowDeduction *bool `json:"allow_deduction"` + ResetCycle int64 `json:"reset_cycle"` + RenewalReset *bool `json:"renewal_reset"` + ShowOriginalPrice bool `json:"show_original_price"` } SubscribeSortRequest { Sort []SortItem `json:"sort"` diff --git a/apis/public/user.api b/apis/public/user.api index 3612328..a02b758 100644 --- a/apis/public/user.api +++ b/apis/public/user.api @@ -66,7 +66,6 @@ type ( UnbindOAuthRequest { Method string `json:"method"` } - GetLoginLogRequest { Page int `form:"page"` Size int `form:"size"` diff --git a/apis/types.api b/apis/types.api index b212734..cf39322 100644 --- a/apis/types.api +++ b/apis/types.api @@ -208,30 +208,31 @@ type ( Discount int64 `json:"discount"` } Subscribe { - Id int64 `json:"id"` - Name string `json:"name"` - Language string `json:"language"` - Description string `json:"description"` - UnitPrice int64 `json:"unit_price"` - UnitTime string `json:"unit_time"` - Discount []SubscribeDiscount `json:"discount"` - Replacement int64 `json:"replacement"` - Inventory int64 `json:"inventory"` - Traffic int64 `json:"traffic"` - SpeedLimit int64 `json:"speed_limit"` - DeviceLimit int64 `json:"device_limit"` - Quota int64 `json:"quota"` - Nodes []int64 `json:"nodes"` - NodeTags []string `json:"node_tags"` - Show bool `json:"show"` - Sell bool `json:"sell"` - Sort int64 `json:"sort"` - DeductionRatio int64 `json:"deduction_ratio"` - AllowDeduction bool `json:"allow_deduction"` - ResetCycle int64 `json:"reset_cycle"` - RenewalReset bool `json:"renewal_reset"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` + Id int64 `json:"id"` + Name string `json:"name"` + Language string `json:"language"` + Description string `json:"description"` + UnitPrice int64 `json:"unit_price"` + UnitTime string `json:"unit_time"` + Discount []SubscribeDiscount `json:"discount"` + Replacement int64 `json:"replacement"` + Inventory int64 `json:"inventory"` + Traffic int64 `json:"traffic"` + SpeedLimit int64 `json:"speed_limit"` + DeviceLimit int64 `json:"device_limit"` + Quota int64 `json:"quota"` + Nodes []int64 `json:"nodes"` + NodeTags []string `json:"node_tags"` + Show bool `json:"show"` + Sell bool `json:"sell"` + Sort int64 `json:"sort"` + DeductionRatio int64 `json:"deduction_ratio"` + AllowDeduction bool `json:"allow_deduction"` + ResetCycle int64 `json:"reset_cycle"` + RenewalReset bool `json:"renewal_reset"` + ShowOriginalPrice bool `json:"show_original_price"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` } SubscribeGroup { Id int64 `json:"id"` diff --git a/initialize/migrate/database/02123_subscribe_original.down.sql b/initialize/migrate/database/02123_subscribe_original.down.sql new file mode 100644 index 0000000..2527d48 --- /dev/null +++ b/initialize/migrate/database/02123_subscribe_original.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE `subscribe` +DROP COLUMN `show_original_price`; diff --git a/initialize/migrate/database/02123_subscribe_original.up.sql b/initialize/migrate/database/02123_subscribe_original.up.sql new file mode 100644 index 0000000..af04a8b --- /dev/null +++ b/initialize/migrate/database/02123_subscribe_original.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE `subscribe` + ADD COLUMN `show_original_price` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'display the original price: 0 not display, 1 display' AFTER `created_at`; diff --git a/internal/logic/admin/subscribe/createSubscribeLogic.go b/internal/logic/admin/subscribe/createSubscribeLogic.go index bf50d6a..6309e2b 100644 --- a/internal/logic/admin/subscribe/createSubscribeLogic.go +++ b/internal/logic/admin/subscribe/createSubscribeLogic.go @@ -35,28 +35,29 @@ func (l *CreateSubscribeLogic) CreateSubscribe(req *types.CreateSubscribeRequest discount = string(val) } sub := &subscribe.Subscribe{ - Id: 0, - Name: req.Name, - Language: req.Language, - Description: req.Description, - UnitPrice: req.UnitPrice, - UnitTime: req.UnitTime, - Discount: discount, - Replacement: req.Replacement, - Inventory: req.Inventory, - Traffic: req.Traffic, - SpeedLimit: req.SpeedLimit, - DeviceLimit: req.DeviceLimit, - Quota: req.Quota, - Nodes: tool.Int64SliceToString(req.Nodes), - NodeTags: tool.StringSliceToString(req.NodeTags), - Show: req.Show, - Sell: req.Sell, - Sort: 0, - DeductionRatio: req.DeductionRatio, - AllowDeduction: req.AllowDeduction, - ResetCycle: req.ResetCycle, - RenewalReset: req.RenewalReset, + Id: 0, + Name: req.Name, + Language: req.Language, + Description: req.Description, + UnitPrice: req.UnitPrice, + UnitTime: req.UnitTime, + Discount: discount, + Replacement: req.Replacement, + Inventory: req.Inventory, + Traffic: req.Traffic, + SpeedLimit: req.SpeedLimit, + DeviceLimit: req.DeviceLimit, + Quota: req.Quota, + Nodes: tool.Int64SliceToString(req.Nodes), + NodeTags: tool.StringSliceToString(req.NodeTags), + Show: req.Show, + Sell: req.Sell, + Sort: 0, + DeductionRatio: req.DeductionRatio, + AllowDeduction: req.AllowDeduction, + ResetCycle: req.ResetCycle, + RenewalReset: req.RenewalReset, + ShowOriginalPrice: req.ShowOriginalPrice, } err := l.svcCtx.SubscribeModel.Insert(l.ctx, sub) if err != nil { diff --git a/internal/logic/admin/subscribe/updateSubscribeLogic.go b/internal/logic/admin/subscribe/updateSubscribeLogic.go index 060af5a..b79fdfe 100644 --- a/internal/logic/admin/subscribe/updateSubscribeLogic.go +++ b/internal/logic/admin/subscribe/updateSubscribeLogic.go @@ -43,28 +43,29 @@ func (l *UpdateSubscribeLogic) UpdateSubscribe(req *types.UpdateSubscribeRequest discount = string(val) } sub := &subscribe.Subscribe{ - Id: req.Id, - Name: req.Name, - Language: req.Language, - Description: req.Description, - UnitPrice: req.UnitPrice, - UnitTime: req.UnitTime, - Discount: discount, - Replacement: req.Replacement, - Inventory: req.Inventory, - Traffic: req.Traffic, - SpeedLimit: req.SpeedLimit, - DeviceLimit: req.DeviceLimit, - Quota: req.Quota, - Nodes: tool.Int64SliceToString(req.Nodes), - NodeTags: tool.StringSliceToString(req.NodeTags), - Show: req.Show, - Sell: req.Sell, - Sort: req.Sort, - DeductionRatio: req.DeductionRatio, - AllowDeduction: req.AllowDeduction, - ResetCycle: req.ResetCycle, - RenewalReset: req.RenewalReset, + Id: req.Id, + Name: req.Name, + Language: req.Language, + Description: req.Description, + UnitPrice: req.UnitPrice, + UnitTime: req.UnitTime, + Discount: discount, + Replacement: req.Replacement, + Inventory: req.Inventory, + Traffic: req.Traffic, + SpeedLimit: req.SpeedLimit, + DeviceLimit: req.DeviceLimit, + Quota: req.Quota, + Nodes: tool.Int64SliceToString(req.Nodes), + NodeTags: tool.StringSliceToString(req.NodeTags), + Show: req.Show, + Sell: req.Sell, + Sort: req.Sort, + DeductionRatio: req.DeductionRatio, + AllowDeduction: req.AllowDeduction, + ResetCycle: req.ResetCycle, + RenewalReset: req.RenewalReset, + ShowOriginalPrice: req.ShowOriginalPrice, } err = l.svcCtx.SubscribeModel.Update(l.ctx, sub) if err != nil { diff --git a/internal/model/subscribe/subscribe.go b/internal/model/subscribe/subscribe.go index a80ea63..b895cb7 100644 --- a/internal/model/subscribe/subscribe.go +++ b/internal/model/subscribe/subscribe.go @@ -7,30 +7,31 @@ import ( ) type Subscribe struct { - Id int64 `gorm:"primaryKey"` - Name string `gorm:"type:varchar(255);not null;default:'';comment:Subscribe Name"` - Language string `gorm:"type:varchar(255);not null;default:'';comment:Language"` - 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"` - Quota int64 `gorm:"type:int;not null;default:0;comment:Quota"` - 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"` - DeductionRatio int64 `gorm:"type:int;default:0;comment:Deduction Ratio"` - AllowDeduction *bool `gorm:"type:tinyint(1);default:1;comment:Allow deduction"` - ResetCycle int64 `gorm:"type:int;default:0;comment:Reset Cycle: 0: No Reset, 1: 1st, 2: Monthly, 3: Yearly"` - RenewalReset *bool `gorm:"type:tinyint(1);default:0;comment:Renew Reset"` - CreatedAt time.Time `gorm:"<-:create;comment:Create Time"` - UpdatedAt time.Time `gorm:"comment:Update Time"` + Id int64 `gorm:"primaryKey"` + Name string `gorm:"type:varchar(255);not null;default:'';comment:Subscribe Name"` + Language string `gorm:"type:varchar(255);not null;default:'';comment:Language"` + 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"` + Quota int64 `gorm:"type:int;not null;default:0;comment:Quota"` + 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"` + DeductionRatio int64 `gorm:"type:int;default:0;comment:Deduction Ratio"` + AllowDeduction *bool `gorm:"type:tinyint(1);default:1;comment:Allow deduction"` + ResetCycle int64 `gorm:"type:int;default:0;comment:Reset Cycle: 0: No Reset, 1: 1st, 2: Monthly, 3: Yearly"` + RenewalReset *bool `gorm:"type:tinyint(1);default:0;comment:Renew Reset"` + ShowOriginalPrice bool `gorm:"type:tinyint(1);not null;default:1;comment:Show Original Price"` + CreatedAt time.Time `gorm:"<-:create;comment:Create Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` } func (*Subscribe) TableName() string { diff --git a/internal/types/types.go b/internal/types/types.go index 44180f4..cf15d7d 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -388,26 +388,27 @@ type CreateSubscribeGroupRequest struct { } type CreateSubscribeRequest struct { - Name string `json:"name" validate:"required"` - Language string `json:"language"` - Description string `json:"description"` - UnitPrice int64 `json:"unit_price"` - UnitTime string `json:"unit_time"` - Discount []SubscribeDiscount `json:"discount"` - Replacement int64 `json:"replacement"` - Inventory int64 `json:"inventory"` - Traffic int64 `json:"traffic"` - SpeedLimit int64 `json:"speed_limit"` - DeviceLimit int64 `json:"device_limit"` - Quota int64 `json:"quota"` - Nodes []int64 `json:"nodes"` - NodeTags []string `json:"node_tags"` - Show *bool `json:"show"` - Sell *bool `json:"sell"` - DeductionRatio int64 `json:"deduction_ratio"` - AllowDeduction *bool `json:"allow_deduction"` - ResetCycle int64 `json:"reset_cycle"` - RenewalReset *bool `json:"renewal_reset"` + Name string `json:"name" validate:"required"` + Language string `json:"language"` + Description string `json:"description"` + UnitPrice int64 `json:"unit_price"` + UnitTime string `json:"unit_time"` + Discount []SubscribeDiscount `json:"discount"` + Replacement int64 `json:"replacement"` + Inventory int64 `json:"inventory"` + Traffic int64 `json:"traffic"` + SpeedLimit int64 `json:"speed_limit"` + DeviceLimit int64 `json:"device_limit"` + Quota int64 `json:"quota"` + Nodes []int64 `json:"nodes"` + NodeTags []string `json:"node_tags"` + Show *bool `json:"show"` + Sell *bool `json:"sell"` + DeductionRatio int64 `json:"deduction_ratio"` + AllowDeduction *bool `json:"allow_deduction"` + ResetCycle int64 `json:"reset_cycle"` + RenewalReset *bool `json:"renewal_reset"` + ShowOriginalPrice bool `json:"show_original_price"` } type CreateTicketFollowRequest struct { @@ -2054,30 +2055,31 @@ type StripePayment struct { } type Subscribe struct { - Id int64 `json:"id"` - Name string `json:"name"` - Language string `json:"language"` - Description string `json:"description"` - UnitPrice int64 `json:"unit_price"` - UnitTime string `json:"unit_time"` - Discount []SubscribeDiscount `json:"discount"` - Replacement int64 `json:"replacement"` - Inventory int64 `json:"inventory"` - Traffic int64 `json:"traffic"` - SpeedLimit int64 `json:"speed_limit"` - DeviceLimit int64 `json:"device_limit"` - Quota int64 `json:"quota"` - Nodes []int64 `json:"nodes"` - NodeTags []string `json:"node_tags"` - Show bool `json:"show"` - Sell bool `json:"sell"` - Sort int64 `json:"sort"` - DeductionRatio int64 `json:"deduction_ratio"` - AllowDeduction bool `json:"allow_deduction"` - ResetCycle int64 `json:"reset_cycle"` - RenewalReset bool `json:"renewal_reset"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` + Id int64 `json:"id"` + Name string `json:"name"` + Language string `json:"language"` + Description string `json:"description"` + UnitPrice int64 `json:"unit_price"` + UnitTime string `json:"unit_time"` + Discount []SubscribeDiscount `json:"discount"` + Replacement int64 `json:"replacement"` + Inventory int64 `json:"inventory"` + Traffic int64 `json:"traffic"` + SpeedLimit int64 `json:"speed_limit"` + DeviceLimit int64 `json:"device_limit"` + Quota int64 `json:"quota"` + Nodes []int64 `json:"nodes"` + NodeTags []string `json:"node_tags"` + Show bool `json:"show"` + Sell bool `json:"sell"` + Sort int64 `json:"sort"` + DeductionRatio int64 `json:"deduction_ratio"` + AllowDeduction bool `json:"allow_deduction"` + ResetCycle int64 `json:"reset_cycle"` + RenewalReset bool `json:"renewal_reset"` + ShowOriginalPrice bool `json:"show_original_price"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` } type SubscribeApplication struct { @@ -2433,28 +2435,29 @@ type UpdateSubscribeGroupRequest struct { } type UpdateSubscribeRequest struct { - Id int64 `json:"id" validate:"required"` - Name string `json:"name" validate:"required"` - Language string `json:"language"` - Description string `json:"description"` - UnitPrice int64 `json:"unit_price"` - UnitTime string `json:"unit_time"` - Discount []SubscribeDiscount `json:"discount"` - Replacement int64 `json:"replacement"` - Inventory int64 `json:"inventory"` - Traffic int64 `json:"traffic"` - SpeedLimit int64 `json:"speed_limit"` - DeviceLimit int64 `json:"device_limit"` - Quota int64 `json:"quota"` - Nodes []int64 `json:"nodes"` - NodeTags []string `json:"node_tags"` - Show *bool `json:"show"` - Sell *bool `json:"sell"` - Sort int64 `json:"sort"` - DeductionRatio int64 `json:"deduction_ratio"` - AllowDeduction *bool `json:"allow_deduction"` - ResetCycle int64 `json:"reset_cycle"` - RenewalReset *bool `json:"renewal_reset"` + Id int64 `json:"id" validate:"required"` + Name string `json:"name" validate:"required"` + Language string `json:"language"` + Description string `json:"description"` + UnitPrice int64 `json:"unit_price"` + UnitTime string `json:"unit_time"` + Discount []SubscribeDiscount `json:"discount"` + Replacement int64 `json:"replacement"` + Inventory int64 `json:"inventory"` + Traffic int64 `json:"traffic"` + SpeedLimit int64 `json:"speed_limit"` + DeviceLimit int64 `json:"device_limit"` + Quota int64 `json:"quota"` + Nodes []int64 `json:"nodes"` + NodeTags []string `json:"node_tags"` + Show *bool `json:"show"` + Sell *bool `json:"sell"` + Sort int64 `json:"sort"` + DeductionRatio int64 `json:"deduction_ratio"` + AllowDeduction *bool `json:"allow_deduction"` + ResetCycle int64 `json:"reset_cycle"` + RenewalReset *bool `json:"renewal_reset"` + ShowOriginalPrice bool `json:"show_original_price"` } type UpdateTicketStatusRequest struct { From 21f77e141b82ea82aa5c7a358f0af7fd0d01bca8 Mon Sep 17 00:00:00 2001 From: Tension Date: Sun, 28 Dec 2025 21:52:16 +0800 Subject: [PATCH 32/50] feat(node): update Node Multiplier configuration and initialize node --- internal/logic/admin/system/setNodeMultiplierLogic.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/internal/logic/admin/system/setNodeMultiplierLogic.go b/internal/logic/admin/system/setNodeMultiplierLogic.go index 78edf50..98033fe 100644 --- a/internal/logic/admin/system/setNodeMultiplierLogic.go +++ b/internal/logic/admin/system/setNodeMultiplierLogic.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" + "github.com/perfect-panel/server/initialize" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/logger" @@ -32,9 +33,12 @@ func (l *SetNodeMultiplierLogic) SetNodeMultiplier(req *types.SetNodeMultiplierR l.Logger.Error("Marshal Node Multiplier Config Error: ", logger.Field("error", err.Error())) return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Marshal Node Multiplier Config Error: %s", err.Error()) } - if err := l.svcCtx.SystemModel.UpdateNodeMultiplierConfig(l.ctx, string(data)); err != nil { + if err = l.svcCtx.SystemModel.UpdateNodeMultiplierConfig(l.ctx, string(data)); err != nil { l.Logger.Error("Update Node Multiplier Config Error: ", logger.Field("error", err.Error())) return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Update Node Multiplier Config Error: %s", err.Error()) } + // update Node Multiplier + initialize.Node(l.svcCtx) + return nil } From ec829452c10cb83844ae538ee7829949b332859b Mon Sep 17 00:00:00 2001 From: Tension Date: Sun, 28 Dec 2025 21:52:29 +0800 Subject: [PATCH 33/50] feat(traffic): add debug logging for current time traffic multiplier --- queue/logic/traffic/trafficStatisticsLogic.go | 1 + 1 file changed, 1 insertion(+) diff --git a/queue/logic/traffic/trafficStatisticsLogic.go b/queue/logic/traffic/trafficStatisticsLogic.go index ed98cd1..37614cb 100644 --- a/queue/logic/traffic/trafficStatisticsLogic.go +++ b/queue/logic/traffic/trafficStatisticsLogic.go @@ -79,6 +79,7 @@ func (l *TrafficStatisticsLogic) ProcessTask(ctx context.Context, task *asynq.Ta now := time.Now() realTimeMultiplier := l.svc.NodeMultiplierManager.GetMultiplier(now) + logger.Debugf("[TrafficStatisticsLogic] Current time traffic multiplier: %.2f", realTimeMultiplier) for _, log := range payload.Logs { // query user Subscribe Info sub, err := l.svc.UserModel.FindOneSubscribe(ctx, log.SID) From bbc3703404c9389ef0139db704f50047f4b527f9 Mon Sep 17 00:00:00 2001 From: Tension Date: Sun, 28 Dec 2025 21:52:42 +0800 Subject: [PATCH 34/50] feat(traffic): enhance logging for successful push traffic tasks --- internal/logic/server/serverPushUserTrafficLogic.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/logic/server/serverPushUserTrafficLogic.go b/internal/logic/server/serverPushUserTrafficLogic.go index c6ab4e6..53bc9be 100644 --- a/internal/logic/server/serverPushUserTrafficLogic.go +++ b/internal/logic/server/serverPushUserTrafficLogic.go @@ -51,7 +51,7 @@ func (l *ServerPushUserTrafficLogic) ServerPushUserTraffic(req *types.ServerPush if err != nil { l.Errorw("[ServerPushUserTraffic] Push traffic task error", logger.Field("error", err.Error()), logger.Field("task", t)) } else { - l.Infow("[ServerPushUserTraffic] Push traffic task success", logger.Field("task", t), logger.Field("info", info)) + l.Infow("[ServerPushUserTraffic] Push traffic task success", logger.Field("task", t.Type()), logger.Field("info", string(info.Payload))) } // Update server last reported time From 495c4529eddffb68893e901ef4da77c7f4afef2d Mon Sep 17 00:00:00 2001 From: Tension Date: Sun, 28 Dec 2025 21:52:54 +0800 Subject: [PATCH 35/50] fix(gorm): adjust caller skip for logging methods to improve stack trace accuracy --- internal/logic/admin/system/getNodeConfigLogic.go | 1 + pkg/logger/gorm.go | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/logic/admin/system/getNodeConfigLogic.go b/internal/logic/admin/system/getNodeConfigLogic.go index 1212b1a..6037aa9 100644 --- a/internal/logic/admin/system/getNodeConfigLogic.go +++ b/internal/logic/admin/system/getNodeConfigLogic.go @@ -3,6 +3,7 @@ package system import ( "context" "encoding/json" + "github.com/perfect-panel/server/internal/config" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" diff --git a/pkg/logger/gorm.go b/pkg/logger/gorm.go index 7dbdd4a..c7a295d 100644 --- a/pkg/logger/gorm.go +++ b/pkg/logger/gorm.go @@ -32,15 +32,15 @@ func (l *GormLogger) LogMode(logger.LogLevel) logger.Interface { } func (l *GormLogger) Info(ctx context.Context, str string, args ...interface{}) { - WithContext(ctx).WithCallerSkip(6).Infof("%s Info: %s", TAG, str, args) + WithContext(ctx).WithCallerSkip(2).Infof("%s Info: %s", TAG, str, args) } func (l *GormLogger) Warn(ctx context.Context, str string, args ...interface{}) { - WithContext(ctx).WithCallerSkip(6).Infof("%s Warn: %s", TAG, str, args) + WithContext(ctx).WithCallerSkip(2).Infof("%s Warn: %s", TAG, str, args) } func (l *GormLogger) Error(ctx context.Context, str string, args ...interface{}) { - WithContext(ctx).WithCallerSkip(6).Errorf("%s Error: %s", TAG, str, args) + WithContext(ctx).WithCallerSkip(2).Errorf("%s Error: %s", TAG, str, args) } func (l *GormLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) { From 7a2000f6965abcc713bf955312f9d51ae95b3769 Mon Sep 17 00:00:00 2001 From: Tension Date: Sun, 28 Dec 2025 22:04:50 +0800 Subject: [PATCH 36/50] feat(discount): change discount type to float64 for improved precision --- apis/types.api | 4 ++-- internal/logic/public/order/getDiscount.go | 4 ++-- internal/logic/public/portal/tool.go | 4 ++-- internal/types/types.go | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apis/types.api b/apis/types.api index cf39322..5f174be 100644 --- a/apis/types.api +++ b/apis/types.api @@ -204,8 +204,8 @@ type ( CurrencySymbol string `json:"currency_symbol"` } SubscribeDiscount { - Quantity int64 `json:"quantity"` - Discount int64 `json:"discount"` + Quantity int64 `json:"quantity"` + Discount float64 `json:"discount"` } Subscribe { Id int64 `json:"id"` diff --git a/internal/logic/public/order/getDiscount.go b/internal/logic/public/order/getDiscount.go index 34c16a9..01f1035 100644 --- a/internal/logic/public/order/getDiscount.go +++ b/internal/logic/public/order/getDiscount.go @@ -3,7 +3,7 @@ package order import "github.com/perfect-panel/server/internal/types" func getDiscount(discounts []types.SubscribeDiscount, inputMonths int64) float64 { - var finalDiscount int64 = 100 + var finalDiscount float64 = 100 for _, discount := range discounts { if inputMonths >= discount.Quantity && discount.Discount < finalDiscount { @@ -11,5 +11,5 @@ func getDiscount(discounts []types.SubscribeDiscount, inputMonths int64) float64 } } - return float64(finalDiscount) / float64(100) + return finalDiscount / float64(100) } diff --git a/internal/logic/public/portal/tool.go b/internal/logic/public/portal/tool.go index c2d2bbd..f687e2a 100644 --- a/internal/logic/public/portal/tool.go +++ b/internal/logic/public/portal/tool.go @@ -7,14 +7,14 @@ import ( ) func getDiscount(discounts []types.SubscribeDiscount, inputMonths int64) float64 { - var finalDiscount int64 = 100 + var finalDiscount float64 = 100 for _, discount := range discounts { if inputMonths >= discount.Quantity && discount.Discount < finalDiscount { finalDiscount = discount.Discount } } - return float64(finalDiscount) / float64(100) + return finalDiscount / float64(100) } func calculateCoupon(amount int64, couponInfo *coupon.Coupon) int64 { diff --git a/internal/types/types.go b/internal/types/types.go index cf15d7d..6717c18 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -2117,8 +2117,8 @@ type SubscribeConfig struct { } type SubscribeDiscount struct { - Quantity int64 `json:"quantity"` - Discount int64 `json:"discount"` + Quantity int64 `json:"quantity"` + Discount float64 `json:"discount"` } type SubscribeGroup struct { From fb6adc9ae353bc4d09096a0eaf0cd164ac91cbed Mon Sep 17 00:00:00 2001 From: Tension Date: Sun, 28 Dec 2025 22:44:14 +0800 Subject: [PATCH 37/50] feat(subscribe): add inventory check and update logic for subscription plans --- .../logic/public/order/closeOrderLogic.go | 22 +++++++++++++++++++ internal/logic/public/order/purchaseLogic.go | 19 ++++++++++++++++ internal/logic/public/portal/purchaseLogic.go | 16 ++++++++++++++ pkg/xerr/errCode.go | 1 + pkg/xerr/errMsg.go | 1 + 5 files changed, 59 insertions(+) diff --git a/internal/logic/public/order/closeOrderLogic.go b/internal/logic/public/order/closeOrderLogic.go index ced53b8..dd7ea13 100644 --- a/internal/logic/public/order/closeOrderLogic.go +++ b/internal/logic/public/order/closeOrderLogic.go @@ -51,6 +51,16 @@ func (l *CloseOrderLogic) CloseOrder(req *types.CloseOrderRequest) error { ) return nil } + + sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, orderInfo.SubscribeId) + if err != nil { + l.Errorw("[CloseOrder] Find subscribe info failed", + logger.Field("error", err.Error()), + logger.Field("subscribeId", orderInfo.SubscribeId), + ) + return nil + } + err = l.svcCtx.DB.Transaction(func(tx *gorm.DB) error { // update order status err := tx.Model(&order.Order{}).Where("order_no = ?", req.OrderNo).Update("status", 3).Error @@ -124,9 +134,21 @@ func (l *CloseOrderLogic) CloseOrder(req *types.CloseOrderRequest) error { // update user cache return l.svcCtx.UserModel.UpdateUserCache(l.ctx, userInfo) } + if sub.Inventory != -1 { + sub.Inventory++ + if e := l.svcCtx.SubscribeModel.Update(l.ctx, sub, tx); e != nil { + l.Errorw("[CloseOrder] Restore subscribe inventory failed", + logger.Field("error", e.Error()), + logger.Field("subscribeId", sub.Id), + ) + return e + } + } + return nil }) if err != nil { + logger.Errorf("[CloseOrder] Transaction failed: %v", err.Error()) return err } return nil diff --git a/internal/logic/public/order/purchaseLogic.go b/internal/logic/public/order/purchaseLogic.go index 519a80a..cbc960f 100644 --- a/internal/logic/public/order/purchaseLogic.go +++ b/internal/logic/public/order/purchaseLogic.go @@ -81,6 +81,12 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P if !*sub.Sell { return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "subscribe not sell") } + + // check subscribe plan inventory + if sub.Inventory == 0 { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SubscribeOutOfStock), "subscribe out of stock") + } + // check subscribe plan limit if sub.Quota > 0 { var count int64 @@ -221,10 +227,23 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P return e } } + + if sub.Inventory != -1 { + // decrease subscribe plan stock + sub.Inventory -= 1 + // update subscribe plan stock + if err = l.svcCtx.SubscribeModel.Update(l.ctx, sub, db); err != nil { + l.Errorw("[Purchase] Database update error", logger.Field("error", err.Error()), logger.Field("subscribe", sub)) + return err + } + } + // insert order return db.WithContext(l.ctx).Model(&order.Order{}).Create(&orderInfo).Error }) if err != nil { + l.Errorw("[Purchase] Database insert error", logger.Field("error", err.Error()), logger.Field("orderInfo", orderInfo)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert order error: %v", err.Error()) } // Deferred task diff --git a/internal/logic/public/portal/purchaseLogic.go b/internal/logic/public/portal/purchaseLogic.go index 322f94c..c21cb69 100644 --- a/internal/logic/public/portal/purchaseLogic.go +++ b/internal/logic/public/portal/purchaseLogic.go @@ -55,6 +55,12 @@ func (l *PurchaseLogic) Purchase(req *types.PortalPurchaseRequest) (resp *types. l.Errorw("[Purchase] Database query error", logger.Field("error", err.Error()), logger.Field("subscribe_id", req.SubscribeId)) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe error: %v", err.Error()) } + + // check subscribe plan stock + if sub.Inventory == 0 { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SubscribeOutOfStock), "subscribe out of stock") + } + // check subscribe plan status if !*sub.Sell { return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "subscribe not sell") @@ -149,6 +155,16 @@ func (l *PurchaseLogic) Purchase(req *types.PortalPurchaseRequest) (resp *types. return err } l.Infow("[Purchase] Guest order", logger.Field("order_no", orderInfo.OrderNo), logger.Field("identifier", req.Identifier)) + + // Decrease subscribe plan stock + if sub.Inventory != -1 { + sub.Inventory-- + if e := l.svcCtx.SubscribeModel.Update(l.ctx, sub, tx); e != nil { + l.Errorw("[Purchase] Database update error", logger.Field("error", e.Error()), logger.Field("subscribe_id", sub.Id)) + return e + } + } + // save guest order if err = l.svcCtx.OrderModel.Insert(l.ctx, orderInfo, tx); err != nil { return err diff --git a/pkg/xerr/errCode.go b/pkg/xerr/errCode.go index 7e1bfd3..fd9d098 100644 --- a/pkg/xerr/errCode.go +++ b/pkg/xerr/errCode.go @@ -70,6 +70,7 @@ const ( SubscribeIsUsedError uint32 = 60004 SingleSubscribeModeExceedsLimit uint32 = 60005 SubscribeQuotaLimit uint32 = 60006 + SubscribeOutOfStock uint32 = 60007 ) // Auth error diff --git a/pkg/xerr/errMsg.go b/pkg/xerr/errMsg.go index f688854..1814720 100644 --- a/pkg/xerr/errMsg.go +++ b/pkg/xerr/errMsg.go @@ -55,6 +55,7 @@ func init() { SubscribeIsUsedError: "Subscribe is used", SingleSubscribeModeExceedsLimit: "Single subscribe mode exceeds limit", SubscribeQuotaLimit: "Subscribe quota limit", + SubscribeOutOfStock: "Subscribe out of stock", // auth error VerifyCodeError: "Verify code error", From 518294a5281e4d207fdeea4db6f71b2326d0c928 Mon Sep 17 00:00:00 2001 From: Tension Date: Sun, 28 Dec 2025 22:55:38 +0800 Subject: [PATCH 38/50] feat(database): add migration to drop server_group table --- initialize/migrate/database/02124_server_group_delete.down.sql | 0 initialize/migrate/database/02124_server_group_delete.up.sql | 1 + 2 files changed, 1 insertion(+) create mode 100644 initialize/migrate/database/02124_server_group_delete.down.sql create mode 100644 initialize/migrate/database/02124_server_group_delete.up.sql diff --git a/initialize/migrate/database/02124_server_group_delete.down.sql b/initialize/migrate/database/02124_server_group_delete.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/initialize/migrate/database/02124_server_group_delete.up.sql b/initialize/migrate/database/02124_server_group_delete.up.sql new file mode 100644 index 0000000..57c0b5a --- /dev/null +++ b/initialize/migrate/database/02124_server_group_delete.up.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS `server_group`; \ No newline at end of file From 577490749c9f89ef2a7aab7a25782b26c46290fa Mon Sep 17 00:00:00 2001 From: Tension Date: Mon, 29 Dec 2025 13:29:12 +0800 Subject: [PATCH 39/50] feat(subscribe): update inventory logic in subscribe table and add migration scripts --- initialize/migrate/database/02125_subscribe_stock.down.sql | 5 +++++ initialize/migrate/database/02125_subscribe_stock.up.sql | 4 ++++ internal/model/subscribe/subscribe.go | 2 +- 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 initialize/migrate/database/02125_subscribe_stock.down.sql create mode 100644 initialize/migrate/database/02125_subscribe_stock.up.sql diff --git a/initialize/migrate/database/02125_subscribe_stock.down.sql b/initialize/migrate/database/02125_subscribe_stock.down.sql new file mode 100644 index 0000000..c8482cc --- /dev/null +++ b/initialize/migrate/database/02125_subscribe_stock.down.sql @@ -0,0 +1,5 @@ + +-- This migration script reverts the inventory values in the 'subscribe' table +UPDATE `subscribe` +SET `inventory` = 0 +WHERE `inventory` = -1; \ No newline at end of file diff --git a/initialize/migrate/database/02125_subscribe_stock.up.sql b/initialize/migrate/database/02125_subscribe_stock.up.sql new file mode 100644 index 0000000..88fead1 --- /dev/null +++ b/initialize/migrate/database/02125_subscribe_stock.up.sql @@ -0,0 +1,4 @@ +-- Update the `subscribe` table to set `inventory` to -1 where it is currently 0 +UPDATE `subscribe` +SET `inventory` = -1 +WHERE `inventory` = 0; \ No newline at end of file diff --git a/internal/model/subscribe/subscribe.go b/internal/model/subscribe/subscribe.go index b895cb7..c9c1046 100644 --- a/internal/model/subscribe/subscribe.go +++ b/internal/model/subscribe/subscribe.go @@ -15,7 +15,7 @@ type Subscribe struct { 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"` + Inventory int64 `gorm:"type:int;not null;default:-1;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"` From e8084e9d2c5c255054682e5322fbbcffbd97e153 Mon Sep 17 00:00:00 2001 From: Tension Date: Mon, 29 Dec 2025 13:50:50 +0800 Subject: [PATCH 40/50] feat(subscribe): rename stop user subscribe handler to toggle and update logic for status change --- apis/admin/user.api | 6 ++--- ...go => toggleUserSubscribeStatusHandler.go} | 8 +++---- internal/handler/routes.go | 2 +- ...c.go => toggleUserSubscribeStatusLogic.go} | 22 ++++++++++++++----- internal/types/types.go | 8 +++---- 5 files changed, 28 insertions(+), 18 deletions(-) rename internal/handler/admin/user/{stopUserSubscribeHandler.go => toggleUserSubscribeStatusHandler.go} (66%) rename internal/logic/admin/user/{stopUserSubscribeLogic.go => toggleUserSubscribeStatusLogic.go} (67%) diff --git a/apis/admin/user.api b/apis/admin/user.api index 1f01a6f..923f197 100644 --- a/apis/admin/user.api +++ b/apis/admin/user.api @@ -184,7 +184,7 @@ type ( GetUserSubscribeByIdRequest { Id int64 `form:"id" validate:"required"` } - StopUserSubscribeRequest { + ToggleUserSubscribeStatusRequest { UserSubscribeId int64 `json:"user_subscribe_id"` } ResetUserSubscribeTrafficRequest { @@ -304,8 +304,8 @@ service ppanel { post /subscribe/reset/token (ResetUserSubscribeTokenRequest) @doc "Stop user subscribe" - @handler StopUserSubscribe - post /subscribe/stop (StopUserSubscribeRequest) + @handler ToggleUserSubscribeStatus + post /subscribe/toggle (ToggleUserSubscribeStatusRequest) @doc "Reset user subscribe traffic" @handler ResetUserSubscribeTraffic diff --git a/internal/handler/admin/user/stopUserSubscribeHandler.go b/internal/handler/admin/user/toggleUserSubscribeStatusHandler.go similarity index 66% rename from internal/handler/admin/user/stopUserSubscribeHandler.go rename to internal/handler/admin/user/toggleUserSubscribeStatusHandler.go index d61f04a..5883e99 100644 --- a/internal/handler/admin/user/stopUserSubscribeHandler.go +++ b/internal/handler/admin/user/toggleUserSubscribeStatusHandler.go @@ -9,9 +9,9 @@ import ( ) // Stop user subscribe -func StopUserSubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { +func ToggleUserSubscribeStatusHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - var req types.StopUserSubscribeRequest + var req types.ToggleUserSubscribeStatusRequest _ = c.ShouldBind(&req) validateErr := svcCtx.Validate(&req) if validateErr != nil { @@ -19,8 +19,8 @@ func StopUserSubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return } - l := user.NewStopUserSubscribeLogic(c.Request.Context(), svcCtx) - err := l.StopUserSubscribe(&req) + l := user.NewToggleUserSubscribeStatusLogic(c.Request.Context(), svcCtx) + err := l.ToggleUserSubscribeStatus(&req) result.HttpResult(c, nil, err) } } diff --git a/internal/handler/routes.go b/internal/handler/routes.go index 2d41010..6a942a7 100644 --- a/internal/handler/routes.go +++ b/internal/handler/routes.go @@ -583,7 +583,7 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { adminUserGroupRouter.POST("/subscribe/reset/traffic", adminUser.ResetUserSubscribeTrafficHandler(serverCtx)) // Stop user subscribe - adminUserGroupRouter.POST("/subscribe/stop", adminUser.StopUserSubscribeHandler(serverCtx)) + adminUserGroupRouter.POST("/subscribe/toggle", adminUser.ToggleUserSubscribeStatusHandler(serverCtx)) // Get user subcribe traffic logs adminUserGroupRouter.GET("/subscribe/traffic_logs", adminUser.GetUserSubscribeTrafficLogsHandler(serverCtx)) diff --git a/internal/logic/admin/user/stopUserSubscribeLogic.go b/internal/logic/admin/user/toggleUserSubscribeStatusLogic.go similarity index 67% rename from internal/logic/admin/user/stopUserSubscribeLogic.go rename to internal/logic/admin/user/toggleUserSubscribeStatusLogic.go index 658e4cf..5f06e98 100644 --- a/internal/logic/admin/user/stopUserSubscribeLogic.go +++ b/internal/logic/admin/user/toggleUserSubscribeStatusLogic.go @@ -10,28 +10,38 @@ import ( "github.com/pkg/errors" ) -type StopUserSubscribeLogic struct { +type ToggleUserSubscribeStatusLogic struct { logger.Logger ctx context.Context svcCtx *svc.ServiceContext } -// NewStopUserSubscribeLogic Stop user subscribe -func NewStopUserSubscribeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *StopUserSubscribeLogic { - return &StopUserSubscribeLogic{ +// NewToggleUserSubscribeStatusLogic Stop user subscribe +func NewToggleUserSubscribeStatusLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ToggleUserSubscribeStatusLogic { + return &ToggleUserSubscribeStatusLogic{ Logger: logger.WithContext(ctx), ctx: ctx, svcCtx: svcCtx, } } -func (l *StopUserSubscribeLogic) StopUserSubscribe(req *types.StopUserSubscribeRequest) error { +func (l *ToggleUserSubscribeStatusLogic) ToggleUserSubscribeStatus(req *types.ToggleUserSubscribeStatusRequest) error { userSub, err := l.svcCtx.UserModel.FindOneSubscribe(l.ctx, req.UserSubscribeId) if err != nil { l.Errorw("FindOneSubscribe error", logger.Field("error", err.Error()), logger.Field("userSubscribeId", req.UserSubscribeId)) return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), " FindOneSubscribe error: %v", err.Error()) } - userSub.Status = 5 // set status to stopped + + switch userSub.Status { + case 2: // active + userSub.Status = 5 // set status to stopped + case 5: // stopped + userSub.Status = 2 // set status to active + default: + l.Errorw("invalid user subscribe status", logger.Field("userSubscribeId", req.UserSubscribeId), logger.Field("status", userSub.Status)) + return errors.Wrapf(xerr.NewErrCodeMsg(xerr.ERROR, "invalid subscribe status"), "invalid user subscribe status: %d", userSub.Status) + } + err = l.svcCtx.UserModel.UpdateSubscribe(l.ctx, userSub) if err != nil { l.Errorw("UpdateSubscribe error", logger.Field("error", err.Error()), logger.Field("userSubscribeId", req.UserSubscribeId)) diff --git a/internal/types/types.go b/internal/types/types.go index 6717c18..97042bd 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -2044,10 +2044,6 @@ type StopBatchSendEmailTaskRequest struct { Id int64 `json:"id"` } -type StopUserSubscribeRequest struct { - UserSubscribeId int64 `json:"user_subscribe_id"` -} - type StripePayment struct { Method string `json:"method"` ClientSecret string `json:"client_secret"` @@ -2239,6 +2235,10 @@ type ToggleNodeStatusRequest struct { Enable *bool `json:"enable"` } +type ToggleUserSubscribeStatusRequest struct { + UserSubscribeId int64 `json:"user_subscribe_id"` +} + type TosConfig struct { TosContent string `json:"tos_content"` } From 532a5ab00905656b4891a5d914663db428d8b989 Mon Sep 17 00:00:00 2001 From: Tension Date: Mon, 29 Dec 2025 15:00:19 +0800 Subject: [PATCH 41/50] feat(config): update subscribe path in global config response --- internal/logic/common/getGlobalConfigLogic.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/logic/common/getGlobalConfigLogic.go b/internal/logic/common/getGlobalConfigLogic.go index 61b2c1e..1b55898 100644 --- a/internal/logic/common/getGlobalConfigLogic.go +++ b/internal/logic/common/getGlobalConfigLogic.go @@ -51,6 +51,8 @@ func (l *GetGlobalConfigLogic) GetGlobalConfig() (resp *types.GetGlobalConfigRes tool.SystemConfigSliceReflectToStruct(currencyCfg, &resp.Currency) tool.SystemConfigSliceReflectToStruct(verifyCodeCfg, &resp.VerifyCode) + resp.Subscribe.SubscribePath = "/sub" + l.svcCtx.Config.Subscribe.SubscribePath + resp.Verify = types.VeifyConfig{ TurnstileSiteKey: l.svcCtx.Config.Verify.TurnstileSiteKey, EnableLoginVerify: l.svcCtx.Config.Verify.LoginVerify, From d4c6aa052860c9b870517cee188957b94fd1e3b7 Mon Sep 17 00:00:00 2001 From: Tension Date: Mon, 29 Dec 2025 15:03:39 +0800 Subject: [PATCH 42/50] feat(node): add enabled field to node creation logic --- internal/logic/admin/server/createNodeLogic.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/logic/admin/server/createNodeLogic.go b/internal/logic/admin/server/createNodeLogic.go index f635f85..78ce987 100644 --- a/internal/logic/admin/server/createNodeLogic.go +++ b/internal/logic/admin/server/createNodeLogic.go @@ -31,6 +31,7 @@ func (l *CreateNodeLogic) CreateNode(req *types.CreateNodeRequest) error { data := node.Node{ Name: req.Name, Tags: tool.StringSliceToString(req.Tags), + Enabled: req.Enabled, Port: req.Port, Address: req.Address, ServerId: req.ServerId, From ff2fa573a052046c2a8692b4ee996243e6fb3608 Mon Sep 17 00:00:00 2001 From: Tension Date: Mon, 29 Dec 2025 15:12:13 +0800 Subject: [PATCH 43/50] fix(subscribe): enhance node subscription logic to handle empty tags and log node counts --- internal/logic/subscribe/subscribeLogic.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/internal/logic/subscribe/subscribeLogic.go b/internal/logic/subscribe/subscribeLogic.go index 6a4a6e5..4b884bd 100644 --- a/internal/logic/subscribe/subscribeLogic.go +++ b/internal/logic/subscribe/subscribeLogic.go @@ -209,13 +209,16 @@ func (l *SubscribeLogic) getServers(userSub *user.Subscribe) ([]*node.Node, erro } nodeIds := tool.StringToInt64Slice(subDetails.Nodes) - tags := strings.Split(subDetails.NodeTags, ",") - - l.Debugf("[Generate Subscribe]nodes: %v, NodeTags: %v", nodeIds, tags) + tags := tool.RemoveStringElement(strings.Split(subDetails.NodeTags, ","), "") + l.Debugf("[Generate Subscribe]nodes: %v, NodeTags: %v", len(nodeIds), len(tags)) + if len(nodeIds) == 0 && len(tags) == 0 { + logger.Infow("[Generate Subscribe]no subscribe nodes") + return []*node.Node{}, nil + } enable := true - - _, nodes, err := l.svc.NodeModel.FilterNodeList(l.ctx.Request.Context(), &node.FilterNodeParams{ + var nodes []*node.Node + _, nodes, err = l.svc.NodeModel.FilterNodeList(l.ctx.Request.Context(), &node.FilterNodeParams{ Page: 1, Size: 1000, NodeId: nodeIds, From 8436c2d6eef587285d6fdedcd402fd8ba6ba5d44 Mon Sep 17 00:00:00 2001 From: Tension Date: Tue, 30 Dec 2025 14:06:46 +0800 Subject: [PATCH 44/50] feat(subscribe): add short token generation for user subscriptions --- internal/logic/admin/user/getUserSubscribeLogic.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/logic/admin/user/getUserSubscribeLogic.go b/internal/logic/admin/user/getUserSubscribeLogic.go index 2deb3ac..cd1e733 100644 --- a/internal/logic/admin/user/getUserSubscribeLogic.go +++ b/internal/logic/admin/user/getUserSubscribeLogic.go @@ -41,6 +41,7 @@ func (l *GetUserSubscribeLogic) GetUserSubscribe(req *types.GetUserSubscribeList for _, item := range data { var sub types.UserSubscribe tool.DeepCopy(&sub, item) + sub.Short, _ = tool.FixedUniqueString(item.Token, 8, "") resp.List = append(resp.List, sub) } return From 77a5373d44566e4696b7de51477932ac1f22b9a5 Mon Sep 17 00:00:00 2001 From: Tension Date: Tue, 30 Dec 2025 14:39:15 +0800 Subject: [PATCH 45/50] feat(adapter): add support for additional parameters in Adapter and Client structs --- adapter/adapter.go | 122 +++++++++++---------- adapter/client.go | 14 ++- internal/handler/subscribe.go | 15 +++ internal/logic/subscribe/subscribeLogic.go | 1 + internal/types/subscribe.go | 8 +- 5 files changed, 96 insertions(+), 64 deletions(-) diff --git a/adapter/adapter.go b/adapter/adapter.go index da5d049..b915bd0 100644 --- a/adapter/adapter.go +++ b/adapter/adapter.go @@ -8,16 +8,24 @@ import ( ) type Adapter struct { - SiteName string // 站点名称 - Servers []*node.Node // 服务器列表 - UserInfo User // 用户信息 - ClientTemplate string // 客户端配置模板 - OutputFormat string // 输出格式,默认是 base64 - SubscribeName string // 订阅名称 + Type string // 协议类型 + SiteName string // 站点名称 + Servers []*node.Node // 服务器列表 + UserInfo User // 用户信息 + ClientTemplate string // 客户端配置模板 + OutputFormat string // 输出格式,默认是 base64 + SubscribeName string // 订阅名称 + Params map[string]string // 其他参数 } type Option func(*Adapter) +func WithParams(params map[string]string) Option { + return func(opts *Adapter) { + opts.Params = params + } +} + // WithServers 设置服务器列表 func WithServers(servers []*node.Node) Option { return func(opts *Adapter) { @@ -76,6 +84,7 @@ func (adapter *Adapter) Client() (*Client, error) { OutputFormat: adapter.OutputFormat, Proxies: []Proxy{}, UserInfo: adapter.UserInfo, + Params: adapter.Params, } proxies, err := adapter.Proxies(adapter.Servers) @@ -101,55 +110,58 @@ func (adapter *Adapter) Proxies(servers []*node.Node) ([]Proxy, error) { } for _, protocol := range protocols { if protocol.Type == item.Protocol { - proxies = append(proxies, Proxy{ - Sort: item.Sort, - 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, - UpMbps: protocol.UpMbps, - DownMbps: protocol.DownMbps, - DisableSNI: protocol.DisableSNI, - ReduceRtt: protocol.ReduceRtt, - UDPRelayMode: protocol.UDPRelayMode, - CongestionController: protocol.CongestionController, - PaddingScheme: protocol.PaddingScheme, - Multiplex: protocol.Multiplex, - XhttpMode: protocol.XhttpMode, - XhttpExtra: protocol.XhttpExtra, - Encryption: protocol.Encryption, - EncryptionMode: protocol.EncryptionMode, - EncryptionRtt: protocol.EncryptionRtt, - EncryptionTicket: protocol.EncryptionTicket, - EncryptionServerPadding: protocol.EncryptionServerPadding, - EncryptionPrivateKey: protocol.EncryptionPrivateKey, - EncryptionClientPadding: protocol.EncryptionClientPadding, - EncryptionPassword: protocol.EncryptionPassword, - Ratio: protocol.Ratio, - CertMode: protocol.CertMode, - CertDNSProvider: protocol.CertDNSProvider, - CertDNSEnv: protocol.CertDNSEnv, - }) + proxies = append( + proxies, + Proxy{ + Sort: item.Sort, + 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, + UpMbps: protocol.UpMbps, + DownMbps: protocol.DownMbps, + DisableSNI: protocol.DisableSNI, + ReduceRtt: protocol.ReduceRtt, + UDPRelayMode: protocol.UDPRelayMode, + CongestionController: protocol.CongestionController, + PaddingScheme: protocol.PaddingScheme, + Multiplex: protocol.Multiplex, + XhttpMode: protocol.XhttpMode, + XhttpExtra: protocol.XhttpExtra, + Encryption: protocol.Encryption, + EncryptionMode: protocol.EncryptionMode, + EncryptionRtt: protocol.EncryptionRtt, + EncryptionTicket: protocol.EncryptionTicket, + EncryptionServerPadding: protocol.EncryptionServerPadding, + EncryptionPrivateKey: protocol.EncryptionPrivateKey, + EncryptionClientPadding: protocol.EncryptionClientPadding, + EncryptionPassword: protocol.EncryptionPassword, + Ratio: protocol.Ratio, + CertMode: protocol.CertMode, + CertDNSProvider: protocol.CertDNSProvider, + CertDNSEnv: protocol.CertDNSEnv, + }, + ) } } } diff --git a/adapter/client.go b/adapter/client.go index e456898..d267c12 100644 --- a/adapter/client.go +++ b/adapter/client.go @@ -93,12 +93,13 @@ type User struct { } type Client struct { - SiteName string // Name of the site - SubscribeName string // Name of the subscription - ClientTemplate string // Template for the entire client configuration - OutputFormat string // json, yaml, etc. - Proxies []Proxy // List of proxy configurations - UserInfo User // User information + SiteName string // Name of the site + SubscribeName string // Name of the subscription + ClientTemplate string // Template for the entire client configuration + OutputFormat string // json, yaml, etc. + Proxies []Proxy // List of proxy configurations + UserInfo User // User information + Params map[string]string // Additional parameters } func (c *Client) Build() ([]byte, error) { @@ -119,6 +120,7 @@ func (c *Client) Build() ([]byte, error) { "OutputFormat": c.OutputFormat, "Proxies": proxies, "UserInfo": c.UserInfo, + "Params": c.Params, }) if err != nil { return nil, err diff --git a/internal/handler/subscribe.go b/internal/handler/subscribe.go index 6c228ed..790ed1b 100644 --- a/internal/handler/subscribe.go +++ b/internal/handler/subscribe.go @@ -23,6 +23,10 @@ func SubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { ua := c.GetHeader("User-Agent") req.UA = c.Request.Header.Get("User-Agent") req.Flag = c.Query("flag") + req.Type = c.Query("type") + // 获取所有查询参数 + req.Params = getQueryMap(c.Request) + if svcCtx.Config.Subscribe.PanDomain { domain := c.Request.Host domainArr := strings.Split(domain, ".") @@ -94,3 +98,14 @@ func RegisterSubscribeHandlers(router *gin.Engine, serverCtx *svc.ServiceContext } router.GET(path, SubscribeHandler(serverCtx)) } + +// GetQueryMap 将 http.Request 的查询参数转换为 map[string]string +func getQueryMap(r *http.Request) map[string]string { + result := make(map[string]string) + for k, v := range r.URL.Query() { + if len(v) > 0 { + result[k] = v[0] + } + } + return result +} diff --git a/internal/logic/subscribe/subscribeLogic.go b/internal/logic/subscribe/subscribeLogic.go index 4b884bd..29a5a94 100644 --- a/internal/logic/subscribe/subscribeLogic.go +++ b/internal/logic/subscribe/subscribeLogic.go @@ -108,6 +108,7 @@ func (l *SubscribeLogic) Handler(req *types.SubscribeRequest) (resp *types.Subsc Traffic: userSubscribe.Traffic, SubscribeURL: l.getSubscribeV2URL(req.Token), }), + adapter.WithParams(req.Params), ) // Get client config diff --git a/internal/types/subscribe.go b/internal/types/subscribe.go index 0e8ab26..ec1bfd9 100644 --- a/internal/types/subscribe.go +++ b/internal/types/subscribe.go @@ -2,9 +2,11 @@ package types type ( SubscribeRequest struct { - Flag string - Token string - UA string + Flag string + Token string + Type string + UA string + Params map[string]string } SubscribeResponse struct { Config []byte From 24f3c29fad2fea3b3532910759b830dec1f78978 Mon Sep 17 00:00:00 2001 From: Tension Date: Tue, 30 Dec 2025 16:23:07 +0800 Subject: [PATCH 46/50] fix(subscribe): improve short token validation by adding case-insensitive comparison --- internal/handler/subscribe.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/handler/subscribe.go b/internal/handler/subscribe.go index 790ed1b..2db3297 100644 --- a/internal/handler/subscribe.go +++ b/internal/handler/subscribe.go @@ -37,7 +37,8 @@ func SubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { c.Abort() return } - if short != domainArr[0] { + if strings.ToLower(short) != strings.ToLower(domainArr[0]) { + logger.Debugf("[SubscribeHandler] Generate short token failed, short: %s, domain: %s", short, domainArr[0]) c.String(http.StatusForbidden, "Access denied") c.Abort() return From 780e71441d19042977562f7e9a8b536ad2dcce2c Mon Sep 17 00:00:00 2001 From: Tension Date: Wed, 31 Dec 2025 10:47:30 +0800 Subject: [PATCH 47/50] fix(subscribe): refactor getSubscribeV2URL to remove token parameter and adjust URL construction --- internal/logic/subscribe/subscribeLogic.go | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/internal/logic/subscribe/subscribeLogic.go b/internal/logic/subscribe/subscribeLogic.go index 29a5a94..28f9ecb 100644 --- a/internal/logic/subscribe/subscribeLogic.go +++ b/internal/logic/subscribe/subscribeLogic.go @@ -10,6 +10,7 @@ import ( "github.com/perfect-panel/server/internal/model/client" "github.com/perfect-panel/server/internal/model/log" "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/internal/report" "github.com/perfect-panel/server/internal/model/user" @@ -106,11 +107,13 @@ func (l *SubscribeLogic) Handler(req *types.SubscribeRequest) (resp *types.Subsc Download: userSubscribe.Download, Upload: userSubscribe.Upload, Traffic: userSubscribe.Traffic, - SubscribeURL: l.getSubscribeV2URL(req.Token), + SubscribeURL: l.getSubscribeV2URL(), }), adapter.WithParams(req.Params), ) + logger.Debugf("[SubscribeLogic] Building client config for user %d with URI %s", userSubscribe.UserId, l.getSubscribeV2URL()) + // Get client config adapterClient, err := a.Client() if err != nil { @@ -144,17 +147,20 @@ func (l *SubscribeLogic) Handler(req *types.SubscribeRequest) (resp *types.Subsc return } -func (l *SubscribeLogic) getSubscribeV2URL(token string) string { - if l.svc.Config.Subscribe.PanDomain { - return fmt.Sprintf("https://%s", l.ctx.Request.Host) - } +func (l *SubscribeLogic) getSubscribeV2URL() string { + uri := l.ctx.Request.RequestURI + // is gateway mode, add /sub prefix + if report.IsGatewayMode() { + uri = "/sub" + uri + } + // use custom domain if configured if l.svc.Config.Subscribe.SubscribeDomain != "" { domains := strings.Split(l.svc.Config.Subscribe.SubscribeDomain, "\n") - return fmt.Sprintf("https://%s%s?token=%s", domains[0], l.svc.Config.Subscribe.SubscribePath, token) + return fmt.Sprintf("https://%s%s", domains[0], uri) } - - return fmt.Sprintf("https://%s%s?token=%s&", l.ctx.Request.Host, l.svc.Config.Subscribe.SubscribePath, token) + // use current request host + return fmt.Sprintf("https://%s%s", l.ctx.Request.Host, uri) } func (l *SubscribeLogic) getUserSubscribe(token string) (*user.Subscribe, error) { From 798fb9e245041e1ca61509291bf3d2ff547311e7 Mon Sep 17 00:00:00 2001 From: Tension Date: Wed, 31 Dec 2025 11:47:24 +0800 Subject: [PATCH 48/50] feat(currency): add currency configuration support and integrate into payment processing --- initialize/currency.go | 34 ++++++++++ initialize/init.go | 1 + internal/config/config.go | 7 ++ .../public/portal/purchaseCheckoutLogic.go | 64 ++++++++----------- internal/svc/serviceContext.go | 2 +- 5 files changed, 71 insertions(+), 37 deletions(-) create mode 100644 initialize/currency.go diff --git a/initialize/currency.go b/initialize/currency.go new file mode 100644 index 0000000..25dc52f --- /dev/null +++ b/initialize/currency.go @@ -0,0 +1,34 @@ +package initialize + +import ( + "context" + "fmt" + + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" +) + +func Currency(ctx *svc.ServiceContext) { + // Retrieve system currency configuration + currency, err := ctx.SystemModel.GetCurrencyConfig(context.Background()) + if err != nil { + logger.Errorf("[INIT] Failed to get currency configuration: %v", err.Error()) + panic(fmt.Sprintf("[INIT] Failed to get currency configuration: %v", err.Error())) + } + // Parse currency configuration + configs := struct { + CurrencyUnit string + CurrencySymbol string + AccessKey string + }{} + tool.SystemConfigSliceReflectToStruct(currency, &configs) + + ctx.Config.Currency = config.Currency{ + Unit: configs.CurrencyUnit, + Symbol: configs.CurrencySymbol, + AccessKey: configs.AccessKey, + } + logger.Infof("[INIT] Currency configuration: %v", ctx.Config.Currency) +} diff --git a/initialize/init.go b/initialize/init.go index 8023ce5..2333b5e 100644 --- a/initialize/init.go +++ b/initialize/init.go @@ -15,6 +15,7 @@ func StartInitSystemConfig(svc *svc.ServiceContext) { Subscribe(svc) Register(svc) Mobile(svc) + Currency(svc) if !svc.Config.Debug { Telegram(svc) } diff --git a/internal/config/config.go b/internal/config/config.go index 59ece74..fc93da5 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -29,6 +29,7 @@ type Config struct { Invite InviteConfig `yaml:"Invite"` Telegram Telegram `yaml:"Telegram"` Log Log `yaml:"Log"` + Currency Currency `yaml:"Currency"` Administrator struct { Email string `yaml:"Email" default:"admin@ppanel.dev"` Password string `yaml:"Password" default:"password"` @@ -241,3 +242,9 @@ type NodeDBConfig struct { Block string Outbound string } + +type Currency struct { + Unit string `yaml:"Unit" default:"CNY"` + Symbol string `yaml:"Symbol" default:"USD"` + AccessKey string `yaml:"AccessKey" default:""` +} diff --git a/internal/logic/public/portal/purchaseCheckoutLogic.go b/internal/logic/public/portal/purchaseCheckoutLogic.go index ff7faf2..105e1dc 100644 --- a/internal/logic/public/portal/purchaseCheckoutLogic.go +++ b/internal/logic/public/portal/purchaseCheckoutLogic.go @@ -9,6 +9,7 @@ import ( "github.com/perfect-panel/server/internal/model/log" "github.com/perfect-panel/server/internal/report" "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/exchangeRate" paymentPlatform "github.com/perfect-panel/server/pkg/payment" @@ -21,12 +22,10 @@ import ( "github.com/perfect-panel/server/internal/model/payment" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/exchangeRate" "github.com/perfect-panel/server/pkg/logger" "github.com/perfect-panel/server/pkg/payment/alipay" "github.com/perfect-panel/server/pkg/payment/epay" "github.com/perfect-panel/server/pkg/payment/stripe" - "github.com/perfect-panel/server/pkg/tool" "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -261,6 +260,7 @@ func (l *PurchaseCheckoutLogic) stripePayment(config string, info *order.Order, // epayPayment processes EPay payment by generating a payment URL for redirect // It handles currency conversion and creates a payment URL for external payment processing func (l *PurchaseCheckoutLogic) epayPayment(config *payment.Payment, info *order.Order, returnUrl string) (string, error) { + var err error // Parse EPay configuration from payment settings epayConfig := &payment.EPayConfig{} if err := epayConfig.Unmarshal([]byte(config.Config)); err != nil { @@ -269,15 +269,18 @@ func (l *PurchaseCheckoutLogic) epayPayment(config *payment.Payment, info *order } // Initialize EPay client with merchant credentials client := epay.NewClient(epayConfig.Pid, epayConfig.Url, epayConfig.Key, epayConfig.Type) - - // Convert order amount to CNY using current exchange rate - amount, err := l.queryExchangeRate("CNY", info.Amount) - if err != nil { - return "", err + var amount float64 + if l.svcCtx.Config.Currency.Unit != "CNY" { + // Convert order amount to CNY using current exchange rate + amount, err = l.queryExchangeRate("CNY", info.Amount) + if err != nil { + return "", err + } + } else { + amount = float64(info.Amount) / float64(100) } // gateway mod - isGatewayMod := report.IsGatewayMode() // Build notification URL for payment status callbacks @@ -293,7 +296,6 @@ func (l *PurchaseCheckoutLogic) epayPayment(config *payment.Payment, info *order if !ok { host = l.svcCtx.Config.Host } - notifyUrl = "https://" + host if isGatewayMod { notifyUrl += "/api" @@ -316,6 +318,7 @@ func (l *PurchaseCheckoutLogic) epayPayment(config *payment.Payment, info *order // CryptoSaaSPayment processes CryptoSaaSPayment payment by generating a payment URL for redirect // It handles currency conversion and creates a payment URL for external payment processing func (l *PurchaseCheckoutLogic) CryptoSaaSPayment(config *payment.Payment, info *order.Order, returnUrl string) (string, error) { + var err error // Parse EPay configuration from payment settings epayConfig := &payment.CryptoSaaSConfig{} if err := epayConfig.Unmarshal([]byte(config.Config)); err != nil { @@ -325,10 +328,16 @@ func (l *PurchaseCheckoutLogic) CryptoSaaSPayment(config *payment.Payment, info // Initialize EPay client with merchant credentials client := epay.NewClient(epayConfig.AccountID, epayConfig.Endpoint, epayConfig.SecretKey, epayConfig.Type) - // Convert order amount to CNY using current exchange rate - amount, err := l.queryExchangeRate("CNY", info.Amount) - if err != nil { - return "", err + var amount float64 + + if l.svcCtx.Config.Currency.Unit != "CNY" { + // Convert order amount to CNY using current exchange rate + amount, err = l.queryExchangeRate("CNY", info.Amount) + if err != nil { + return "", err + } + } else { + amount = float64(info.Amount) / float64(100) } // gateway mod @@ -377,35 +386,18 @@ func (l *PurchaseCheckoutLogic) queryExchangeRate(to string, src int64) (amount return amount, nil } - // Retrieve system currency configuration - currency, err := l.svcCtx.SystemModel.GetCurrencyConfig(l.ctx) - if err != nil { - l.Errorw("[PurchaseCheckout] GetCurrencyConfig error", logger.Field("error", err.Error())) - return 0, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetCurrencyConfig error: %s", err.Error()) - } - - // Parse currency configuration - configs := struct { - CurrencyUnit string - CurrencySymbol string - AccessKey string - }{} - tool.SystemConfigSliceReflectToStruct(currency, &configs) - // Skip conversion if no exchange rate API key configured - if configs.AccessKey == "" { + if l.svcCtx.Config.Currency.AccessKey == "" { return amount, nil } // Convert currency if system currency differs from target currency - if configs.CurrencyUnit != to { - result, err := exchangeRate.GetExchangeRete(configs.CurrencyUnit, to, configs.AccessKey, 1) - if err != nil { - return 0, err - } - amount = result * amount + result, err := exchangeRate.GetExchangeRete(l.svcCtx.Config.Currency.Unit, to, l.svcCtx.Config.Currency.AccessKey, 1) + if err != nil { + return 0, err } - return amount, nil + l.svcCtx.ExchangeRate = result + return result * amount, nil } // balancePayment processes balance payment with gift amount priority logic diff --git a/internal/svc/serviceContext.go b/internal/svc/serviceContext.go index 4f6bc3a..aa79ccc 100644 --- a/internal/svc/serviceContext.go +++ b/internal/svc/serviceContext.go @@ -97,7 +97,7 @@ func NewServiceContext(c config.Config) *ServiceContext { Redis: rds, Config: c, Queue: NewAsynqClient(c), - ExchangeRate: 1.0, + ExchangeRate: 0, GeoIP: geoIP, //NodeCache: cache.NewNodeCacheClient(rds), AuthLimiter: authLimiter, From 90e2f24d46744c9b8ea4060d086d30c597b957ae Mon Sep 17 00:00:00 2001 From: Tension Date: Sat, 3 Jan 2026 18:05:17 +0800 Subject: [PATCH 49/50] fix(config): conditionally set SubscribePath based on gateway mode --- internal/logic/common/getGlobalConfigLogic.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/logic/common/getGlobalConfigLogic.go b/internal/logic/common/getGlobalConfigLogic.go index 1b55898..393371b 100644 --- a/internal/logic/common/getGlobalConfigLogic.go +++ b/internal/logic/common/getGlobalConfigLogic.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" + "github.com/perfect-panel/server/internal/report" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/logger" @@ -51,7 +52,9 @@ func (l *GetGlobalConfigLogic) GetGlobalConfig() (resp *types.GetGlobalConfigRes tool.SystemConfigSliceReflectToStruct(currencyCfg, &resp.Currency) tool.SystemConfigSliceReflectToStruct(verifyCodeCfg, &resp.VerifyCode) - resp.Subscribe.SubscribePath = "/sub" + l.svcCtx.Config.Subscribe.SubscribePath + if report.IsGatewayMode() { + resp.Subscribe.SubscribePath = "/sub" + l.svcCtx.Config.Subscribe.SubscribePath + } resp.Verify = types.VeifyConfig{ TurnstileSiteKey: l.svcCtx.Config.Verify.TurnstileSiteKey, From 8a804eec0c6757f2006b9781c71efe15d5b3a910 Mon Sep 17 00:00:00 2001 From: EUForest Date: Tue, 6 Jan 2026 17:02:31 +0800 Subject: [PATCH 50/50] chore: simplify build workflow for v1.3 --- .github/workflows/deploy-linux.yml | 108 +++++------------------------ 1 file changed, 19 insertions(+), 89 deletions(-) diff --git a/.github/workflows/deploy-linux.yml b/.github/workflows/deploy-linux.yml index f11e368..f3c5e53 100644 --- a/.github/workflows/deploy-linux.yml +++ b/.github/workflows/deploy-linux.yml @@ -11,13 +11,6 @@ on: description: 'Version to build (leave empty for auto)' required: false type: string - create_release: - description: 'Create GitHub Release' - required: false - default: false - type: boolean - release: - types: [ published ] permissions: contents: write @@ -27,109 +20,46 @@ jobs: name: Build Linux Binary runs-on: ubuntu-latest - outputs: - version: ${{ steps.version.outputs.version }} - binary_name: ${{ steps.build.outputs.binary_name }} - steps: - - name: Checkout repository + - name: Checkout uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Set up Go + - name: Setup Go uses: actions/setup-go@v5 with: go-version: '1.23.3' cache: true - - name: Get version information - id: version - run: | - if [ "${{ github.event_name }}" = "release" ]; then - VERSION=${GITHUB_REF#refs/tags/} - elif [ "${{ github.event_name }}" = "push" ] && [ "${{ github.ref_type }}" = "tag" ]; then - VERSION=${GITHUB_REF#refs/tags/} - elif [ -n "${{ github.event.inputs.version }}" ]; then - VERSION="${{ github.event.inputs.version }}" - else - VERSION=$(git describe --tags --always --dirty) - fi - echo "VERSION=$VERSION" >> $GITHUB_ENV - echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "BUILD_TIME=$(date -u +"%Y-%m-%d %H:%M:%S")" >> $GITHUB_ENV - echo "GIT_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV - - - name: Build binary - id: build + - name: Build env: CGO_ENABLED: 0 GOOS: linux GOARCH: amd64 run: | - BINARY_NAME_WITH_VERSION="ppanel-server-${{ env.VERSION }}-linux-amd64" - echo "binary_name=$BINARY_NAME_WITH_VERSION" >> $GITHUB_OUTPUT + VERSION=${{ github.event.inputs.version }} + if [ -z "$VERSION" ]; then + VERSION=$(git describe --tags --always --dirty) + fi - go build \ - -ldflags "-s -w \ - -X 'github.com/perfect-panel/server/pkg/constant.Version=${{ env.VERSION }}' \ - -X 'github.com/perfect-panel/server/pkg/constant.BuildTime=${{ env.BUILD_TIME }}' \ - -X 'github.com/perfect-panel/server/pkg/constant.GitCommit=${{ env.GIT_COMMIT }}'" \ - -o "$BINARY_NAME_WITH_VERSION" \ - ./ppanel.go + echo "Building ppanel-server $VERSION" + go build -ldflags "-s -w -X main.Version=$VERSION" -o ppanel-server ./ppanel.go + tar -czf ppanel-server-${VERSION}-linux-amd64.tar.gz ppanel-server + sha256sum ppanel-server ppanel-server-${VERSION}-linux-amd64.tar.gz > checksum.txt - - name: Generate checksum - run: | - sha256sum "${{ steps.build.outputs.binary_name }}" > checksum.txt - echo "Checksum: $(cat checksum.txt)" - - - name: Upload build artifacts + - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: ppanel-server-linux-amd64 path: | - ${{ steps.build.outputs.binary_name }} + ppanel-server + ppanel-server-*-linux-amd64.tar.gz checksum.txt - retention-days: 30 - - name: Create and Upload to GitHub Release - if: github.event_name == 'release' || github.event.inputs.create_release == 'true' || (github.event_name == 'push' && github.ref_type == 'tag') + - name: Create Release + if: startsWith(github.ref, 'refs/tags/') env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - # Create release if it doesn't exist - if [ "${{ github.event.inputs.create_release }}" = "true" ] && [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - echo "Creating release for ${{ env.VERSION }}" - gh release create ${{ env.VERSION }} \ - --title "PPanel Server ${{ env.VERSION }}" \ - --notes "Release ${{ env.VERSION }}" \ - --latest - elif [ "${{ github.event_name }}" = "push" ] && [ "${{ github.ref_type }}" = "tag" ]; then - echo "Creating release for tag ${{ env.VERSION }}" - gh release create ${{ env.VERSION }} \ - --title "PPanel Server ${{ env.VERSION }}" \ - --notes "Release ${{ env.VERSION }}" \ - --latest - fi - - echo "Uploading binaries to release ${{ env.VERSION }}" - gh release upload ${{ env.VERSION }} \ - ${{ steps.build.outputs.binary_name }} \ - checksum.txt - - - name: Create summary - run: | - echo "## 📦 Build Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Binary:** ${{ steps.build.outputs.binary_name }}" >> $GITHUB_STEP_SUMMARY - echo "**Version:** ${{ env.VERSION }}" >> $GITHUB_STEP_SUMMARY - echo "**Size:** $(du -h ${{ steps.build.outputs.binary_name }} | cut -f1)" >> $GITHUB_STEP_SUMMARY - echo "**Checksum:** $(cat checksum.txt)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### 📥 Download" >> $GITHUB_STEP_SUMMARY - echo "Go to Actions → Artifacts → \`ppanel-server-linux-amd64\` to download" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### 🚀 Usage" >> $GITHUB_STEP_SUMMARY - echo "1. Download the binary" >> $GITHUB_STEP_SUMMARY - echo "2. Extract from tar.gz" >> $GITHUB_STEP_SUMMARY - echo "3. Run: chmod +x ppanel-server-* && ./ppanel-server-* run --config etc/ppanel.yaml" >> $GITHUB_STEP_SUMMARY \ No newline at end of file + VERSION=${GITHUB_REF#refs/tags/} + gh release create $VERSION --title "PPanel Server $VERSION" || true + gh release upload $VERSION ppanel-server-${VERSION}-linux-amd64.tar.gz checksum.txt