Merge branch perfect-panel/master/server into develop
This commit is contained in:
parent
5e46357104
commit
76ff9a658d
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -22,6 +22,11 @@ type (
|
||||
CurrentTime string `json:"current_time"`
|
||||
Ratio float32 `json:"ratio"`
|
||||
}
|
||||
ModuleConfig {
|
||||
Secret string `json:"secret"` // 通讯密钥
|
||||
ServiceName string `json:"service_name"` // 服务名称
|
||||
ServiceVersion string `json:"service_version"` // 服务版本
|
||||
}
|
||||
)
|
||||
|
||||
@server (
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -14,11 +14,9 @@ type (
|
||||
QuerySubscribeListRequest {
|
||||
Language string `form:"language"`
|
||||
}
|
||||
|
||||
QueryUserSubscribeNodeListResponse {
|
||||
List []UserSubscribeInfo `json:"list"`
|
||||
}
|
||||
|
||||
UserSubscribeInfo {
|
||||
Id int64 `json:"id"`
|
||||
UserId int64 `json:"user_id"`
|
||||
|
||||
@ -123,6 +123,36 @@ type (
|
||||
LongestSingleConnection int64 `json:"longest_single_connection"`
|
||||
}
|
||||
|
||||
UpdateUserSubscribeNoteRequest {
|
||||
UserSubscribeId int64 `json:"user_subscribe_id" validate:"required"`
|
||||
Note string `json:"note" validate:"max=500"`
|
||||
}
|
||||
|
||||
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 (
|
||||
@ -234,6 +264,22 @@ service ppanel {
|
||||
@doc "Delete Current User Account"
|
||||
@handler DeleteCurrentUserAccount
|
||||
delete /current_user_account
|
||||
|
||||
@doc "Update User Subscribe Note"
|
||||
@handler UpdateUserSubscribeNote
|
||||
put /subscribe_note (UpdateUserSubscribeNoteRequest)
|
||||
|
||||
@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)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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"`
|
||||
@ -135,14 +136,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"`
|
||||
}
|
||||
|
||||
RegisterConfig {
|
||||
StopRegister bool `json:"stop_register"`
|
||||
EnableTrial bool `json:"enable_trial"`
|
||||
@ -472,6 +471,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"`
|
||||
}
|
||||
@ -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"`
|
||||
}
|
||||
@ -673,7 +673,6 @@ type (
|
||||
List []SubscribeGroup `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
GetUserSubscribeTrafficLogsRequest {
|
||||
Page int `form:"page"`
|
||||
Size int `form:"size"`
|
||||
|
||||
BIN
cache/GeoLite2-City.mmdb
vendored
Normal file
BIN
cache/GeoLite2-City.mmdb
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 61 MiB |
73
cmd/update.go
Normal file
73
cmd/update.go
Normal file
@ -0,0 +1,73 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/perfect-panel/server/pkg/updater"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
checkOnly bool
|
||||
)
|
||||
|
||||
var updateCmd = &cobra.Command{
|
||||
Use: "update",
|
||||
Short: "Check for updates and update PPanel to the latest version",
|
||||
Long: `Check for available updates from GitHub releases and automatically
|
||||
update the PPanel binary to the latest version.
|
||||
|
||||
Examples:
|
||||
# Check for updates only
|
||||
ppanel-server update --check
|
||||
|
||||
# Update to the latest version
|
||||
ppanel-server update`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
u := updater.NewUpdater()
|
||||
|
||||
if checkOnly {
|
||||
checkForUpdates(u)
|
||||
return
|
||||
}
|
||||
|
||||
performUpdate(u)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
updateCmd.Flags().BoolVarP(&checkOnly, "check", "c", false, "Check for updates without applying them")
|
||||
}
|
||||
|
||||
func checkForUpdates(u *updater.Updater) {
|
||||
fmt.Println("Checking for updates...")
|
||||
|
||||
release, hasUpdate, err := u.CheckForUpdates()
|
||||
if err != nil {
|
||||
fmt.Printf("Error checking for updates: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !hasUpdate {
|
||||
fmt.Println("You are already running the latest version!")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("\nNew version available!\n")
|
||||
fmt.Printf("Current version: %s\n", u.CurrentVersion)
|
||||
fmt.Printf("Latest version: %s\n", release.TagName)
|
||||
fmt.Printf("\nRelease notes:\n%s\n", release.Body)
|
||||
fmt.Printf("\nTo update, run: ppanel-server update\n")
|
||||
}
|
||||
|
||||
func performUpdate(u *updater.Updater) {
|
||||
fmt.Println("Starting update process...")
|
||||
|
||||
if err := u.Update(); err != nil {
|
||||
fmt.Printf("Update failed: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("\nUpdate completed successfully!")
|
||||
fmt.Println("Please restart the application to use the new version.")
|
||||
}
|
||||
2
go.mod
2
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
|
||||
|
||||
4
go.sum
4
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=
|
||||
|
||||
@ -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
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
ALTER TABLE `user_subscribe`
|
||||
DROP COLUMN `note`;
|
||||
@ -0,0 +1,4 @@
|
||||
ALTER TABLE `user_subscribe`
|
||||
ADD COLUMN `note` VARCHAR(500) NOT NULL DEFAULT ''
|
||||
COMMENT 'User note for subscription'
|
||||
AFTER `status`;
|
||||
2
initialize/migrate/database/02121_user_rules.down.sql
Normal file
2
initialize/migrate/database/02121_user_rules.down.sql
Normal file
@ -0,0 +1,2 @@
|
||||
ALTER TABLE `user`
|
||||
DROP COLUMN IF EXISTS `rules`;
|
||||
4
initialize/migrate/database/02121_user_rules.up.sql
Normal file
4
initialize/migrate/database/02121_user_rules.up.sql
Normal file
@ -0,0 +1,4 @@
|
||||
ALTER TABLE `user`
|
||||
ADD COLUMN `rules` TEXT NULL
|
||||
COMMENT 'User rules for subscription'
|
||||
AFTER `created_at`;
|
||||
@ -0,0 +1,5 @@
|
||||
DROP TABLE IF EXISTS `withdrawals`;
|
||||
|
||||
DELETE FROM `system`
|
||||
WHERE `category` = 'invite'
|
||||
AND `key` = 'WithdrawalMethod';
|
||||
16
initialize/migrate/database/02122_user_withdrawal.up.sql
Normal file
16
initialize/migrate/database/02122_user_withdrawal.up.sql
Normal file
@ -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');
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
18
internal/handler/admin/system/getModuleConfigHandler.go
Normal file
18
internal/handler/admin/system/getModuleConfigHandler.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
26
internal/handler/admin/tool/queryIPLocationHandler.go
Normal file
26
internal/handler/admin/tool/queryIPLocationHandler.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
|
||||
18
internal/handler/common/heartbeatHandler.go
Normal file
18
internal/handler/common/heartbeatHandler.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
26
internal/handler/public/user/commissionWithdrawHandler.go
Normal file
26
internal/handler/public/user/commissionWithdrawHandler.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
26
internal/handler/public/user/queryWithdrawalLogHandler.go
Normal file
26
internal/handler/public/user/queryWithdrawalLogHandler.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
26
internal/handler/public/user/updateUserRulesHandler.go
Normal file
26
internal/handler/public/user/updateUserRulesHandler.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -386,6 +386,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))
|
||||
}
|
||||
@ -409,6 +412,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))
|
||||
|
||||
@ -488,6 +494,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))
|
||||
|
||||
@ -636,6 +645,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))
|
||||
|
||||
@ -796,6 +808,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))
|
||||
|
||||
// Delete Current User Account
|
||||
publicUserGroupRouter.DELETE("/current_user_account", publicUser.DeleteCurrentUserAccountHandler(serverCtx))
|
||||
|
||||
@ -820,12 +835,18 @@ 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))
|
||||
|
||||
// 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))
|
||||
|
||||
@ -846,6 +867,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))
|
||||
}
|
||||
|
||||
publicUserWsGroupRouter := router.Group("/v1/public/user")
|
||||
|
||||
@ -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 == "" {
|
||||
|
||||
@ -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{
|
||||
|
||||
@ -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
|
||||
}
|
||||
41
internal/logic/admin/system/getModuleConfigLogic.go
Normal file
41
internal/logic/admin/system/getModuleConfigLogic.go
Normal file
@ -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
|
||||
}
|
||||
57
internal/logic/admin/tool/queryIPLocationLogic.go
Normal file
57
internal/logic/admin/tool/queryIPLocationLogic.go
Normal file
@ -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
|
||||
}
|
||||
33
internal/logic/common/heartbeatLogic.go
Normal file
33
internal/logic/common/heartbeatLogic.go
Normal file
@ -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
|
||||
}
|
||||
@ -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,
|
||||
|
||||
108
internal/logic/public/user/commissionWithdrawLogic.go
Normal file
108
internal/logic/public/user/commissionWithdrawLogic.go
Normal file
@ -0,0 +1,108 @@
|
||||
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 {
|
||||
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) {
|
||||
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")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
30
internal/logic/public/user/queryWithdrawalLogLogic.go
Normal file
30
internal/logic/public/user/queryWithdrawalLogLogic.go
Normal file
@ -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
|
||||
}
|
||||
51
internal/logic/public/user/updateUserRulesLogic.go
Normal file
51
internal/logic/public/user/updateUserRulesLogic.go
Normal file
@ -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
|
||||
}
|
||||
73
internal/logic/public/user/updateUserSubscribeNoteLogic.go
Normal file
73
internal/logic/public/user/updateUserSubscribeNoteLogic.go
Normal file
@ -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
|
||||
}
|
||||
@ -46,8 +46,9 @@ const (
|
||||
CommissionTypePurchase uint16 = 331 // Purchase
|
||||
CommissionTypeRenewal uint16 = 332 // Renewal
|
||||
CommissionTypeRefund uint16 = 333 // Refund
|
||||
commissionTypeWithdraw uint16 = 334 // withdraw
|
||||
CommissionTypeWithdraw uint16 = 334 // withdraw
|
||||
CommissionTypeAdjust uint16 = 335 // Admin Adjust
|
||||
CommissionTypeConvertBalance uint16 = 336 // Convert to Balance
|
||||
GiftTypeIncrease uint16 = 341 // Increase
|
||||
GiftTypeReduce uint16 = 342 // Reduce
|
||||
)
|
||||
|
||||
@ -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"`
|
||||
}
|
||||
|
||||
@ -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"`
|
||||
}
|
||||
@ -48,6 +49,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"`
|
||||
}
|
||||
@ -100,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"
|
||||
}
|
||||
|
||||
15
internal/report/report.go
Normal file
15
internal/report/report.go
Normal file
@ -0,0 +1,15 @@
|
||||
package report
|
||||
|
||||
const (
|
||||
RegisterAPI = "/basic/register" // 模块注册接口
|
||||
)
|
||||
|
||||
// RegisterResponse 模块注册响应参数
|
||||
type RegisterResponse struct {
|
||||
Code int `json:"code"` // 响应代码
|
||||
Message string `json:"message"` // 响应信息
|
||||
Data struct {
|
||||
Success bool `json:"success"` // 注册是否成功
|
||||
Message string `json:"message"` // 返回信息
|
||||
} `json:"data"` // 响应数据
|
||||
}
|
||||
113
internal/report/tool.go
Normal file
113
internal/report/tool.go
Normal file
@ -0,0 +1,113 @@
|
||||
package report
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
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 {
|
||||
logger.Errorf("Failed to determine GATEWAY_PORT: %v", err)
|
||||
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: 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)
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to register service: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if result.IsError() {
|
||||
return errors.New("failed to register module: " + result.Status())
|
||||
}
|
||||
|
||||
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)
|
||||
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
|
||||
}
|
||||
21
internal/report/tool_test.go
Normal file
21
internal/report/tool_test.go
Normal file
@ -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)
|
||||
}
|
||||
11
internal/report/types.go
Normal file
11
internal/report/types.go
Normal file
@ -0,0 +1,11 @@
|
||||
package report
|
||||
|
||||
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"` // 服务名称
|
||||
HeartbeatURL string `json:"heartbeat_url"` // 心跳检测地址
|
||||
ServiceVersion string `json:"service_version"` // 服务版本
|
||||
}
|
||||
@ -6,8 +6,10 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/perfect-panel/server/internal/report"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
|
||||
"github.com/perfect-panel/server/pkg/proc"
|
||||
@ -48,7 +50,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)
|
||||
@ -65,9 +67,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())
|
||||
os.Exit(1)
|
||||
}
|
||||
logger.Infof("module registered on port %d", port)
|
||||
}
|
||||
|
||||
serverAddr := fmt.Sprintf("%v:%d", host, port)
|
||||
m.server = &http.Server{
|
||||
Addr: serverAddr,
|
||||
Handler: r,
|
||||
|
||||
74
internal/svc/mmdb.go
Normal file
74
internal/svc/mmdb.go
Normal file
@ -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
|
||||
}
|
||||
@ -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),
|
||||
|
||||
@ -239,6 +239,11 @@ type CommissionLog struct {
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
type CommissionWithdrawRequest struct {
|
||||
Amount int64 `json:"amount"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type ConnectionRecords struct {
|
||||
CurrentContinuousDays int64 `json:"current_continuous_days"`
|
||||
HistoryContinuousDays int64 `json:"history_continuous_days"`
|
||||
@ -1169,6 +1174,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"`
|
||||
@ -1232,6 +1243,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"`
|
||||
@ -1551,7 +1568,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"`
|
||||
}
|
||||
@ -1570,6 +1587,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:"region,omitempty"`
|
||||
City string `json:"city"`
|
||||
}
|
||||
|
||||
type QueryNodeTagResponse struct {
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
@ -1712,6 +1739,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"`
|
||||
@ -1771,6 +1808,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"`
|
||||
@ -2464,6 +2505,15 @@ 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"`
|
||||
}
|
||||
|
||||
type UpdateUserSubscribeRequest struct {
|
||||
UserSubscribeId int64 `json:"user_subscribe_id"`
|
||||
SubscribeId int64 `json:"subscribe_id"`
|
||||
@ -2497,6 +2547,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"`
|
||||
@ -2587,6 +2638,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"`
|
||||
}
|
||||
@ -2751,3 +2803,14 @@ type WeeklyStat struct {
|
||||
DayName string `json:"day_name"`
|
||||
Hours float64 `json:"hours"`
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
@ -4,4 +4,6 @@ package constant
|
||||
var (
|
||||
Version = "unknown version"
|
||||
BuildTime = "unknown time"
|
||||
Repository = "https://github.com/perfect-panel/server"
|
||||
ServiceName = "ApiService"
|
||||
)
|
||||
|
||||
38
pkg/tool/string.go
Normal file
38
pkg/tool/string.go
Normal file
@ -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
|
||||
}
|
||||
27
pkg/tool/string_test.go
Normal file
27
pkg/tool/string_test.go
Normal file
@ -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)
|
||||
}
|
||||
382
pkg/updater/updater.go
Normal file
382
pkg/updater/updater.go
Normal file
@ -0,0 +1,382 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/perfect-panel/server/pkg/constant"
|
||||
)
|
||||
|
||||
const (
|
||||
githubAPIURL = "https://api.github.com/repos/OmnTeam/server/releases/latest"
|
||||
githubRelURL = "https://github.com/OmnTeam/server/releases"
|
||||
)
|
||||
|
||||
// Release represents a GitHub release
|
||||
type Release struct {
|
||||
TagName string `json:"tag_name"`
|
||||
Name string `json:"name"`
|
||||
Body string `json:"body"`
|
||||
Draft bool `json:"draft"`
|
||||
Prerelease bool `json:"prerelease"`
|
||||
Assets []Asset `json:"assets"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
}
|
||||
|
||||
// Asset represents a release asset
|
||||
type Asset struct {
|
||||
Name string `json:"name"`
|
||||
BrowserDownloadURL string `json:"browser_download_url"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
// Updater handles auto-update functionality
|
||||
type Updater struct {
|
||||
CurrentVersion string
|
||||
Owner string
|
||||
Repo string
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
// NewUpdater creates a new updater instance
|
||||
func NewUpdater() *Updater {
|
||||
return &Updater{
|
||||
CurrentVersion: constant.Version,
|
||||
Owner: "OmnTeam",
|
||||
Repo: "server",
|
||||
HTTPClient: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CheckForUpdates checks if a new version is available
|
||||
func (u *Updater) CheckForUpdates() (*Release, bool, error) {
|
||||
req, err := http.NewRequest("GET", githubAPIURL, nil)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/vnd.github.v3+json")
|
||||
|
||||
resp, err := u.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("failed to fetch release info: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var release Release
|
||||
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
|
||||
return nil, false, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
// Skip draft and prerelease versions
|
||||
if release.Draft || release.Prerelease {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
// Compare versions
|
||||
hasUpdate := u.compareVersions(release.TagName, u.CurrentVersion)
|
||||
return &release, hasUpdate, nil
|
||||
}
|
||||
|
||||
// compareVersions compares two version strings
|
||||
// Returns true if newVersion is newer than currentVersion
|
||||
func (u *Updater) compareVersions(newVersion, currentVersion string) bool {
|
||||
// Remove 'v' prefix if present
|
||||
newVersion = strings.TrimPrefix(newVersion, "v")
|
||||
currentVersion = strings.TrimPrefix(currentVersion, "v")
|
||||
|
||||
// Handle "unknown version" case
|
||||
if currentVersion == "unknown version" || currentVersion == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
return newVersion != currentVersion
|
||||
}
|
||||
|
||||
// Download downloads the appropriate binary for the current platform
|
||||
func (u *Updater) Download(release *Release) (string, error) {
|
||||
assetName := u.getAssetName()
|
||||
|
||||
var targetAsset *Asset
|
||||
for _, asset := range release.Assets {
|
||||
if asset.Name == assetName {
|
||||
targetAsset = &asset
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetAsset == nil {
|
||||
return "", fmt.Errorf("no suitable asset found for %s", assetName)
|
||||
}
|
||||
|
||||
// Create temp directory
|
||||
tempDir, err := os.MkdirTemp("", "ppanel-update-*")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create temp directory: %w", err)
|
||||
}
|
||||
|
||||
// Download the file
|
||||
resp, err := u.HTTPClient.Get(targetAsset.BrowserDownloadURL)
|
||||
if err != nil {
|
||||
os.RemoveAll(tempDir)
|
||||
return "", fmt.Errorf("failed to download asset: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
os.RemoveAll(tempDir)
|
||||
return "", fmt.Errorf("failed to download: status code %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Read the entire file into memory
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
os.RemoveAll(tempDir)
|
||||
return "", fmt.Errorf("failed to read download: %w", err)
|
||||
}
|
||||
|
||||
// Extract the binary
|
||||
binaryPath, err := u.extractBinary(data, tempDir, assetName)
|
||||
if err != nil {
|
||||
os.RemoveAll(tempDir)
|
||||
return "", fmt.Errorf("failed to extract binary: %w", err)
|
||||
}
|
||||
|
||||
return binaryPath, nil
|
||||
}
|
||||
|
||||
// getAssetName returns the expected asset name for the current platform
|
||||
func (u *Updater) getAssetName() string {
|
||||
goos := runtime.GOOS
|
||||
goarch := runtime.GOARCH
|
||||
|
||||
// Capitalize first letter of OS
|
||||
osName := strings.Title(goos)
|
||||
|
||||
// Map architecture names to match goreleaser output
|
||||
archName := goarch
|
||||
switch goarch {
|
||||
case "amd64":
|
||||
archName = "x86_64"
|
||||
case "386":
|
||||
archName = "i386"
|
||||
}
|
||||
|
||||
// Format: ppanel-server-{Version}-{Os}-{Arch}.{ext}
|
||||
ext := "tar.gz"
|
||||
if goos == "windows" {
|
||||
ext = "zip"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("ppanel-server-%s-%s-%s.%s", u.CurrentVersion, osName, archName, ext)
|
||||
}
|
||||
|
||||
// extractBinary extracts the binary from the downloaded archive
|
||||
func (u *Updater) extractBinary(data []byte, destDir, assetName string) (string, error) {
|
||||
if strings.HasSuffix(assetName, ".zip") {
|
||||
return u.extractZip(data, destDir)
|
||||
}
|
||||
return u.extractTarGz(data, destDir)
|
||||
}
|
||||
|
||||
// extractZip extracts a zip archive
|
||||
func (u *Updater) extractZip(data []byte, destDir string) (string, error) {
|
||||
reader := bytes.NewReader(data)
|
||||
zipReader, err := zip.NewReader(reader, int64(len(data)))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create zip reader: %w", err)
|
||||
}
|
||||
|
||||
var binaryPath string
|
||||
for _, file := range zipReader.File {
|
||||
// Look for the binary file
|
||||
if strings.Contains(file.Name, "ppanel-server") && !strings.Contains(file.Name, "/") {
|
||||
binaryPath = filepath.Join(destDir, filepath.Base(file.Name))
|
||||
|
||||
rc, err := file.Open()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to open file in zip: %w", err)
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
outFile, err := os.OpenFile(binaryPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create output file: %w", err)
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
if _, err := io.Copy(outFile, rc); err != nil {
|
||||
return "", fmt.Errorf("failed to write file: %w", err)
|
||||
}
|
||||
|
||||
return binaryPath, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("binary not found in archive")
|
||||
}
|
||||
|
||||
// extractTarGz extracts a tar.gz archive
|
||||
func (u *Updater) extractTarGz(data []byte, destDir string) (string, error) {
|
||||
reader := bytes.NewReader(data)
|
||||
gzReader, err := gzip.NewReader(reader)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create gzip reader: %w", err)
|
||||
}
|
||||
defer gzReader.Close()
|
||||
|
||||
tarReader := tar.NewReader(gzReader)
|
||||
|
||||
var binaryPath string
|
||||
for {
|
||||
header, err := tarReader.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read tar: %w", err)
|
||||
}
|
||||
|
||||
// Look for the binary file
|
||||
if strings.Contains(header.Name, "ppanel-server") && !strings.Contains(header.Name, "/") {
|
||||
binaryPath = filepath.Join(destDir, filepath.Base(header.Name))
|
||||
|
||||
outFile, err := os.OpenFile(binaryPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create output file: %w", err)
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
if _, err := io.Copy(outFile, tarReader); err != nil {
|
||||
return "", fmt.Errorf("failed to write file: %w", err)
|
||||
}
|
||||
|
||||
return binaryPath, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("binary not found in archive")
|
||||
}
|
||||
|
||||
// Apply applies the update by replacing the current binary
|
||||
func (u *Updater) Apply(newBinaryPath string) error {
|
||||
// Get current executable path
|
||||
currentPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get current executable path: %w", err)
|
||||
}
|
||||
|
||||
// Resolve symlinks
|
||||
currentPath, err = filepath.EvalSymlinks(currentPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve symlinks: %w", err)
|
||||
}
|
||||
|
||||
// Create backup
|
||||
backupPath := currentPath + ".backup"
|
||||
if err := u.copyFile(currentPath, backupPath); err != nil {
|
||||
return fmt.Errorf("failed to create backup: %w", err)
|
||||
}
|
||||
|
||||
// Replace the binary
|
||||
if err := u.replaceFile(newBinaryPath, currentPath); err != nil {
|
||||
// Restore backup on failure
|
||||
u.copyFile(backupPath, currentPath)
|
||||
return fmt.Errorf("failed to replace binary: %w", err)
|
||||
}
|
||||
|
||||
// Remove backup on success
|
||||
os.Remove(backupPath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// copyFile copies a file from src to dst
|
||||
func (u *Updater) copyFile(src, dst string) error {
|
||||
srcFile, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
dstFile, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dstFile.Close()
|
||||
|
||||
if _, err := io.Copy(dstFile, srcFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return dstFile.Sync()
|
||||
}
|
||||
|
||||
// replaceFile replaces dst with src
|
||||
func (u *Updater) replaceFile(src, dst string) error {
|
||||
// On Windows, we need to rename the old file first
|
||||
if runtime.GOOS == "windows" {
|
||||
oldPath := dst + ".old"
|
||||
if err := os.Rename(dst, oldPath); err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(oldPath)
|
||||
}
|
||||
|
||||
// Copy the new file
|
||||
if err := u.copyFile(src, dst); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update performs the complete update process
|
||||
func (u *Updater) Update() error {
|
||||
// Check for updates
|
||||
release, hasUpdate, err := u.CheckForUpdates()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check for updates: %w", err)
|
||||
}
|
||||
|
||||
if !hasUpdate {
|
||||
return fmt.Errorf("already running the latest version")
|
||||
}
|
||||
|
||||
fmt.Printf("New version available: %s\n", release.TagName)
|
||||
fmt.Printf("Downloading update...\n")
|
||||
|
||||
// Download the update
|
||||
binaryPath, err := u.Download(release)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to download update: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(filepath.Dir(binaryPath))
|
||||
|
||||
fmt.Printf("Applying update...\n")
|
||||
|
||||
// Apply the update
|
||||
if err := u.Apply(binaryPath); err != nil {
|
||||
return fmt.Errorf("failed to apply update: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Update completed successfully! Please restart the application.\n")
|
||||
return nil
|
||||
}
|
||||
74
pkg/updater/updater_test.go
Normal file
74
pkg/updater/updater_test.go
Normal file
@ -0,0 +1,74 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewUpdater(t *testing.T) {
|
||||
u := NewUpdater()
|
||||
assert.NotNil(t, u)
|
||||
assert.Equal(t, "OmnTeam", u.Owner)
|
||||
assert.Equal(t, "server", u.Repo)
|
||||
assert.NotNil(t, u.HTTPClient)
|
||||
}
|
||||
|
||||
func TestCompareVersions(t *testing.T) {
|
||||
u := NewUpdater()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
newVersion string
|
||||
currentVersion string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "same version",
|
||||
newVersion: "v1.0.0",
|
||||
currentVersion: "v1.0.0",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "different version",
|
||||
newVersion: "v1.1.0",
|
||||
currentVersion: "v1.0.0",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "unknown current version",
|
||||
newVersion: "v1.0.0",
|
||||
currentVersion: "unknown version",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "version without v prefix",
|
||||
newVersion: "1.1.0",
|
||||
currentVersion: "1.0.0",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "empty current version",
|
||||
newVersion: "v1.0.0",
|
||||
currentVersion: "",
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := u.compareVersions(tt.newVersion, tt.currentVersion)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAssetName(t *testing.T) {
|
||||
u := NewUpdater()
|
||||
u.CurrentVersion = "v1.0.0"
|
||||
|
||||
assetName := u.getAssetName()
|
||||
assert.NotEmpty(t, assetName)
|
||||
assert.Contains(t, assetName, "ppanel-server")
|
||||
assert.Contains(t, assetName, "v1.0.0")
|
||||
}
|
||||
@ -27,7 +27,8 @@ const (
|
||||
TelegramNotBound uint32 = 20007
|
||||
UserNotBindOauth uint32 = 20008
|
||||
InviteCodeError uint32 = 20009
|
||||
RegisterIPLimit uint32 = 20010
|
||||
UserCommissionNotEnough uint32 = 20010
|
||||
RegisterIPLimit uint32 = 20011
|
||||
)
|
||||
|
||||
// Node error
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user