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,
|
HopPorts: protocol.HopPorts,
|
||||||
HopInterval: protocol.HopInterval,
|
HopInterval: protocol.HopInterval,
|
||||||
ObfsPassword: protocol.ObfsPassword,
|
ObfsPassword: protocol.ObfsPassword,
|
||||||
|
UpMbps: protocol.UpMbps,
|
||||||
|
DownMbps: protocol.DownMbps,
|
||||||
DisableSNI: protocol.DisableSNI,
|
DisableSNI: protocol.DisableSNI,
|
||||||
ReduceRtt: protocol.ReduceRtt,
|
ReduceRtt: protocol.ReduceRtt,
|
||||||
UDPRelayMode: protocol.UDPRelayMode,
|
UDPRelayMode: protocol.UDPRelayMode,
|
||||||
CongestionController: protocol.CongestionController,
|
CongestionController: protocol.CongestionController,
|
||||||
UpMbps: protocol.UpMbps,
|
|
||||||
DownMbps: protocol.DownMbps,
|
|
||||||
PaddingScheme: protocol.PaddingScheme,
|
PaddingScheme: protocol.PaddingScheme,
|
||||||
Multiplex: protocol.Multiplex,
|
Multiplex: protocol.Multiplex,
|
||||||
XhttpMode: protocol.XhttpMode,
|
XhttpMode: protocol.XhttpMode,
|
||||||
@ -145,6 +145,10 @@ func (adapter *Adapter) Proxies(servers []*node.Node) ([]Proxy, error) {
|
|||||||
EncryptionPrivateKey: protocol.EncryptionPrivateKey,
|
EncryptionPrivateKey: protocol.EncryptionPrivateKey,
|
||||||
EncryptionClientPadding: protocol.EncryptionClientPadding,
|
EncryptionClientPadding: protocol.EncryptionClientPadding,
|
||||||
EncryptionPassword: protocol.EncryptionPassword,
|
EncryptionPassword: protocol.EncryptionPassword,
|
||||||
|
Ratio: protocol.Ratio,
|
||||||
|
CertMode: protocol.CertMode,
|
||||||
|
CertDNSProvider: protocol.CertDNSProvider,
|
||||||
|
CertDNSEnv: protocol.CertDNSEnv,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -102,6 +102,9 @@ type (
|
|||||||
BatchDeleteSubscribeRequest {
|
BatchDeleteSubscribeRequest {
|
||||||
Ids []int64 `json:"ids" validate:"required"`
|
Ids []int64 `json:"ids" validate:"required"`
|
||||||
}
|
}
|
||||||
|
ResetAllSubscribeTokenResponse {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@server (
|
@server (
|
||||||
@ -157,5 +160,9 @@ service ppanel {
|
|||||||
@doc "Subscribe sort"
|
@doc "Subscribe sort"
|
||||||
@handler SubscribeSort
|
@handler SubscribeSort
|
||||||
post /sort (SubscribeSortRequest)
|
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"`
|
CurrentTime string `json:"current_time"`
|
||||||
Ratio float32 `json:"ratio"`
|
Ratio float32 `json:"ratio"`
|
||||||
}
|
}
|
||||||
|
ModuleConfig {
|
||||||
|
Secret string `json:"secret"` // 通讯密钥
|
||||||
|
ServiceName string `json:"service_name"` // 服务名称
|
||||||
|
ServiceVersion string `json:"service_version"` // 服务版本
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@server (
|
@server (
|
||||||
@ -125,5 +130,9 @@ service ppanel {
|
|||||||
@doc "PreView Node Multiplier"
|
@doc "PreView Node Multiplier"
|
||||||
@handler PreViewNodeMultiplier
|
@handler PreViewNodeMultiplier
|
||||||
get /node_multiplier/preview returns (PreViewNodeMultiplierResponse)
|
get /node_multiplier/preview returns (PreViewNodeMultiplierResponse)
|
||||||
|
|
||||||
|
@doc "Get Module Config"
|
||||||
|
@handler GetModuleConfig
|
||||||
|
get /module returns (ModuleConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -17,6 +17,14 @@ type (
|
|||||||
VersionResponse {
|
VersionResponse {
|
||||||
Version string `json:"version"`
|
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 (
|
@server (
|
||||||
@ -36,5 +44,9 @@ service ppanel {
|
|||||||
@doc "Get Version"
|
@doc "Get Version"
|
||||||
@handler GetVersion
|
@handler GetVersion
|
||||||
get /version returns (VersionResponse)
|
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"`
|
Total int64 `json:"total"`
|
||||||
List []SubscribeClient `json:"list"`
|
List []SubscribeClient `json:"list"`
|
||||||
}
|
}
|
||||||
|
HeartbeatResponse {
|
||||||
|
Status bool `json:"status"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
Timestamp int64 `json:"timestamp,omitempty"`
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@server (
|
@server (
|
||||||
@ -130,5 +135,9 @@ service ppanel {
|
|||||||
@doc "Get Client"
|
@doc "Get Client"
|
||||||
@handler GetClient
|
@handler GetClient
|
||||||
get /client returns (GetSubscribeClientResponse)
|
get /client returns (GetSubscribeClientResponse)
|
||||||
|
|
||||||
|
@doc "Heartbeat"
|
||||||
|
@handler Heartbeat
|
||||||
|
get /heartbeat returns (HeartbeatResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,11 +14,9 @@ type (
|
|||||||
QuerySubscribeListRequest {
|
QuerySubscribeListRequest {
|
||||||
Language string `form:"language"`
|
Language string `form:"language"`
|
||||||
}
|
}
|
||||||
|
|
||||||
QueryUserSubscribeNodeListResponse {
|
QueryUserSubscribeNodeListResponse {
|
||||||
List []UserSubscribeInfo `json:"list"`
|
List []UserSubscribeInfo `json:"list"`
|
||||||
}
|
}
|
||||||
|
|
||||||
UserSubscribeInfo {
|
UserSubscribeInfo {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
UserId int64 `json:"user_id"`
|
UserId int64 `json:"user_id"`
|
||||||
|
|||||||
@ -123,6 +123,36 @@ type (
|
|||||||
LongestSingleConnection int64 `json:"longest_single_connection"`
|
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 (
|
@server (
|
||||||
@ -234,6 +264,22 @@ service ppanel {
|
|||||||
@doc "Delete Current User Account"
|
@doc "Delete Current User Account"
|
||||||
@handler DeleteCurrentUserAccount
|
@handler DeleteCurrentUserAccount
|
||||||
delete /current_user_account
|
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"`
|
EnableTradeNotify bool `json:"enable_trade_notify"`
|
||||||
AuthMethods []UserAuthMethod `json:"auth_methods"`
|
AuthMethods []UserAuthMethod `json:"auth_methods"`
|
||||||
UserDevices []UserDevice `json:"user_devices"`
|
UserDevices []UserDevice `json:"user_devices"`
|
||||||
|
Rules []string `json:"rules"`
|
||||||
CreatedAt int64 `json:"created_at"`
|
CreatedAt int64 `json:"created_at"`
|
||||||
UpdatedAt int64 `json:"updated_at"`
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
DeletedAt int64 `json:"deleted_at,omitempty"`
|
DeletedAt int64 `json:"deleted_at,omitempty"`
|
||||||
@ -135,14 +136,12 @@ type (
|
|||||||
EnableDomainSuffix bool `json:"enable_domain_suffix"`
|
EnableDomainSuffix bool `json:"enable_domain_suffix"`
|
||||||
DomainSuffixList string `json:"domain_suffix_list"`
|
DomainSuffixList string `json:"domain_suffix_list"`
|
||||||
}
|
}
|
||||||
|
|
||||||
DeviceAuthticateConfig {
|
DeviceAuthticateConfig {
|
||||||
Enable bool `json:"enable"`
|
Enable bool `json:"enable"`
|
||||||
ShowAds bool `json:"show_ads"`
|
ShowAds bool `json:"show_ads"`
|
||||||
EnableSecurity bool `json:"enable_security"`
|
EnableSecurity bool `json:"enable_security"`
|
||||||
OnlyRealDevice bool `json:"only_real_device"`
|
OnlyRealDevice bool `json:"only_real_device"`
|
||||||
}
|
}
|
||||||
|
|
||||||
RegisterConfig {
|
RegisterConfig {
|
||||||
StopRegister bool `json:"stop_register"`
|
StopRegister bool `json:"stop_register"`
|
||||||
EnableTrial bool `json:"enable_trial"`
|
EnableTrial bool `json:"enable_trial"`
|
||||||
@ -472,6 +471,7 @@ type (
|
|||||||
Upload int64 `json:"upload"`
|
Upload int64 `json:"upload"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
Status uint8 `json:"status"`
|
Status uint8 `json:"status"`
|
||||||
|
Short string `json:"short"`
|
||||||
CreatedAt int64 `json:"created_at"`
|
CreatedAt int64 `json:"created_at"`
|
||||||
UpdatedAt int64 `json:"updated_at"`
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
}
|
}
|
||||||
@ -656,7 +656,7 @@ type (
|
|||||||
// public announcement
|
// public announcement
|
||||||
QueryAnnouncementRequest {
|
QueryAnnouncementRequest {
|
||||||
Page int `form:"page"`
|
Page int `form:"page"`
|
||||||
Size int `form:"size,default=15"`
|
Size int `form:"size"`
|
||||||
Pinned *bool `form:"pinned"`
|
Pinned *bool `form:"pinned"`
|
||||||
Popup *bool `form:"popup"`
|
Popup *bool `form:"popup"`
|
||||||
}
|
}
|
||||||
@ -673,7 +673,6 @@ type (
|
|||||||
List []SubscribeGroup `json:"list"`
|
List []SubscribeGroup `json:"list"`
|
||||||
Total int64 `json:"total"`
|
Total int64 `json:"total"`
|
||||||
}
|
}
|
||||||
|
|
||||||
GetUserSubscribeTrafficLogsRequest {
|
GetUserSubscribeTrafficLogsRequest {
|
||||||
Page int `form:"page"`
|
Page int `form:"page"`
|
||||||
Size int `form:"size"`
|
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/fatih/color v1.18.0
|
||||||
github.com/goccy/go-json v0.10.4
|
github.com/goccy/go-json v0.10.4
|
||||||
github.com/golang-migrate/migrate/v4 v4.18.2
|
github.com/golang-migrate/migrate/v4 v4.18.2
|
||||||
|
github.com/oschwald/geoip2-golang v1.13.0
|
||||||
github.com/spaolacci/murmur3 v1.1.0
|
github.com/spaolacci/murmur3 v1.1.0
|
||||||
google.golang.org/grpc v1.64.1
|
google.golang.org/grpc v1.64.1
|
||||||
google.golang.org/protobuf v1.36.3
|
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/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/openzipkin/zipkin-go v0.4.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/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/robfig/cron/v3 v3.0.1 // 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/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 h1:zjqfqHjUpPmB3c1GlCvvgsM1G4LkvqQbBDueDOCg/jA=
|
||||||
github.com/openzipkin/zipkin-go v0.4.2/go.mod h1:ZeVkFjuuBiSy13y8vpSDCjMi9GoI3hPpCJSBx/EYFhY=
|
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 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/perfect-panel/server/internal/report"
|
||||||
"github.com/perfect-panel/server/pkg/logger"
|
"github.com/perfect-panel/server/pkg/logger"
|
||||||
"gorm.io/driver/mysql"
|
"gorm.io/driver/mysql"
|
||||||
|
|
||||||
@ -35,10 +36,30 @@ func Config(path string) (chan bool, *http.Server) {
|
|||||||
configPath = path
|
configPath = path
|
||||||
// Create a new Gin instance
|
// Create a new Gin instance
|
||||||
r := gin.Default()
|
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
|
// Create a new HTTP server
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
Addr: ":8080",
|
Addr: fmt.Sprintf("%s:%d", host, port),
|
||||||
Handler: r,
|
Handler: r,
|
||||||
}
|
}
|
||||||
// Load templates
|
// 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
|
// Get Client
|
||||||
func GetClientHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
func GetClientHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
|
|
||||||
l := common.NewGetClientLogic(c.Request.Context(), svcCtx)
|
l := common.NewGetClientLogic(c.Request.Context(), svcCtx)
|
||||||
resp, err := l.GetClient()
|
resp, err := l.GetClient()
|
||||||
result.HttpResult(c, resp, err)
|
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
|
// Get subscribe list
|
||||||
adminSubscribeGroupRouter.GET("/list", adminSubscribe.GetSubscribeListHandler(serverCtx))
|
adminSubscribeGroupRouter.GET("/list", adminSubscribe.GetSubscribeListHandler(serverCtx))
|
||||||
|
|
||||||
|
// Reset all subscribe tokens
|
||||||
|
adminSubscribeGroupRouter.POST("/reset_all_token", adminSubscribe.ResetAllSubscribeTokenHandler(serverCtx))
|
||||||
|
|
||||||
// Subscribe sort
|
// Subscribe sort
|
||||||
adminSubscribeGroupRouter.POST("/sort", adminSubscribe.SubscribeSortHandler(serverCtx))
|
adminSubscribeGroupRouter.POST("/sort", adminSubscribe.SubscribeSortHandler(serverCtx))
|
||||||
}
|
}
|
||||||
@ -409,6 +412,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
|||||||
// Update invite config
|
// Update invite config
|
||||||
adminSystemGroupRouter.PUT("/invite_config", adminSystem.UpdateInviteConfigHandler(serverCtx))
|
adminSystemGroupRouter.PUT("/invite_config", adminSystem.UpdateInviteConfigHandler(serverCtx))
|
||||||
|
|
||||||
|
// Get Module Config
|
||||||
|
adminSystemGroupRouter.GET("/module", adminSystem.GetModuleConfigHandler(serverCtx))
|
||||||
|
|
||||||
// Get node config
|
// Get node config
|
||||||
adminSystemGroupRouter.GET("/node_config", adminSystem.GetNodeConfigHandler(serverCtx))
|
adminSystemGroupRouter.GET("/node_config", adminSystem.GetNodeConfigHandler(serverCtx))
|
||||||
|
|
||||||
@ -488,6 +494,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
|||||||
adminToolGroupRouter.Use(middleware.AuthMiddleware(serverCtx))
|
adminToolGroupRouter.Use(middleware.AuthMiddleware(serverCtx))
|
||||||
|
|
||||||
{
|
{
|
||||||
|
// Query IP Location
|
||||||
|
adminToolGroupRouter.GET("/ip/location", adminTool.QueryIPLocationHandler(serverCtx))
|
||||||
|
|
||||||
// Get System Log
|
// Get System Log
|
||||||
adminToolGroupRouter.GET("/log", adminTool.GetSystemLogHandler(serverCtx))
|
adminToolGroupRouter.GET("/log", adminTool.GetSystemLogHandler(serverCtx))
|
||||||
|
|
||||||
@ -636,6 +645,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
|||||||
// Get Client
|
// Get Client
|
||||||
commonGroupRouter.GET("/client", common.GetClientHandler(serverCtx))
|
commonGroupRouter.GET("/client", common.GetClientHandler(serverCtx))
|
||||||
|
|
||||||
|
// Heartbeat
|
||||||
|
commonGroupRouter.GET("/heartbeat", common.HeartbeatHandler(serverCtx))
|
||||||
|
|
||||||
// Get verification code
|
// Get verification code
|
||||||
commonGroupRouter.POST("/send_code", common.SendEmailCodeHandler(serverCtx))
|
commonGroupRouter.POST("/send_code", common.SendEmailCodeHandler(serverCtx))
|
||||||
|
|
||||||
@ -796,6 +808,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
|||||||
// Query User Commission Log
|
// Query User Commission Log
|
||||||
publicUserGroupRouter.GET("/commission_log", publicUser.QueryUserCommissionLogHandler(serverCtx))
|
publicUserGroupRouter.GET("/commission_log", publicUser.QueryUserCommissionLogHandler(serverCtx))
|
||||||
|
|
||||||
|
// Commission Withdraw
|
||||||
|
publicUserGroupRouter.POST("/commission_withdraw", publicUser.CommissionWithdrawHandler(serverCtx))
|
||||||
|
|
||||||
// Delete Current User Account
|
// Delete Current User Account
|
||||||
publicUserGroupRouter.DELETE("/current_user_account", publicUser.DeleteCurrentUserAccountHandler(serverCtx))
|
publicUserGroupRouter.DELETE("/current_user_account", publicUser.DeleteCurrentUserAccountHandler(serverCtx))
|
||||||
|
|
||||||
@ -820,12 +835,18 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
|||||||
// Update User Password
|
// Update User Password
|
||||||
publicUserGroupRouter.PUT("/password", publicUser.UpdateUserPasswordHandler(serverCtx))
|
publicUserGroupRouter.PUT("/password", publicUser.UpdateUserPasswordHandler(serverCtx))
|
||||||
|
|
||||||
|
// Update User Rules
|
||||||
|
publicUserGroupRouter.PUT("/rules", publicUser.UpdateUserRulesHandler(serverCtx))
|
||||||
|
|
||||||
// Query User Subscribe
|
// Query User Subscribe
|
||||||
publicUserGroupRouter.GET("/subscribe", publicUser.QueryUserSubscribeHandler(serverCtx))
|
publicUserGroupRouter.GET("/subscribe", publicUser.QueryUserSubscribeHandler(serverCtx))
|
||||||
|
|
||||||
// Get Subscribe Log
|
// Get Subscribe Log
|
||||||
publicUserGroupRouter.GET("/subscribe_log", publicUser.GetSubscribeLogHandler(serverCtx))
|
publicUserGroupRouter.GET("/subscribe_log", publicUser.GetSubscribeLogHandler(serverCtx))
|
||||||
|
|
||||||
|
// Update User Subscribe Note
|
||||||
|
publicUserGroupRouter.PUT("/subscribe_note", publicUser.UpdateUserSubscribeNoteHandler(serverCtx))
|
||||||
|
|
||||||
// Reset User Subscribe Token
|
// Reset User Subscribe Token
|
||||||
publicUserGroupRouter.PUT("/subscribe_token", publicUser.ResetUserSubscribeTokenHandler(serverCtx))
|
publicUserGroupRouter.PUT("/subscribe_token", publicUser.ResetUserSubscribeTokenHandler(serverCtx))
|
||||||
|
|
||||||
@ -846,6 +867,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
|||||||
|
|
||||||
// Verify Email
|
// Verify Email
|
||||||
publicUserGroupRouter.POST("/verify_email", publicUser.VerifyEmailHandler(serverCtx))
|
publicUserGroupRouter.POST("/verify_email", publicUser.VerifyEmailHandler(serverCtx))
|
||||||
|
|
||||||
|
// Query Withdrawal Log
|
||||||
|
publicUserGroupRouter.GET("/withdrawal_log", publicUser.QueryWithdrawalLogHandler(serverCtx))
|
||||||
}
|
}
|
||||||
|
|
||||||
publicUserWsGroupRouter := router.Group("/v1/public/user")
|
publicUserWsGroupRouter := router.Group("/v1/public/user")
|
||||||
|
|||||||
@ -23,6 +23,22 @@ func SubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
|||||||
ua := c.GetHeader("User-Agent")
|
ua := c.GetHeader("User-Agent")
|
||||||
req.UA = c.Request.Header.Get("User-Agent")
|
req.UA = c.Request.Header.Get("User-Agent")
|
||||||
req.Flag = c.Query("flag")
|
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 svcCtx.Config.Subscribe.UserAgentLimit {
|
||||||
if ua == "" {
|
if ua == "" {
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/perfect-panel/server/internal/report"
|
||||||
paymentPlatform "github.com/perfect-panel/server/pkg/payment"
|
paymentPlatform "github.com/perfect-panel/server/pkg/payment"
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/model/payment"
|
"github.com/perfect-panel/server/internal/model/payment"
|
||||||
@ -43,15 +44,31 @@ func (l *GetPaymentMethodListLogic) GetPaymentMethodList(req *types.GetPaymentMe
|
|||||||
Total: total,
|
Total: total,
|
||||||
List: make([]types.PaymentMethodDetail, len(list)),
|
List: make([]types.PaymentMethodDetail, len(list)),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// gateway mod
|
||||||
|
|
||||||
|
isGatewayMod := report.IsGatewayMode()
|
||||||
|
|
||||||
for i, v := range list {
|
for i, v := range list {
|
||||||
config := make(map[string]interface{})
|
config := make(map[string]interface{})
|
||||||
_ = json.Unmarshal([]byte(v.Config), &config)
|
_ = json.Unmarshal([]byte(v.Config), &config)
|
||||||
notifyUrl := ""
|
notifyUrl := ""
|
||||||
|
|
||||||
if paymentPlatform.ParsePlatform(v.Platform) != paymentPlatform.Balance {
|
if paymentPlatform.ParsePlatform(v.Platform) != paymentPlatform.Balance {
|
||||||
|
notifyUrl = v.Domain
|
||||||
if 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 {
|
} 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{
|
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"
|
"time"
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/model/log"
|
"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/constant"
|
||||||
|
|
||||||
paymentPlatform "github.com/perfect-panel/server/pkg/payment"
|
paymentPlatform "github.com/perfect-panel/server/pkg/payment"
|
||||||
@ -275,16 +276,29 @@ func (l *PurchaseCheckoutLogic) epayPayment(config *payment.Payment, info *order
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// gateway mod
|
||||||
|
|
||||||
|
isGatewayMod := report.IsGatewayMode()
|
||||||
|
|
||||||
// Build notification URL for payment status callbacks
|
// Build notification URL for payment status callbacks
|
||||||
notifyUrl := ""
|
notifyUrl := ""
|
||||||
if config.Domain != "" {
|
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 {
|
} else {
|
||||||
host, ok := l.ctx.Value(constant.CtxKeyRequestHost).(string)
|
host, ok := l.ctx.Value(constant.CtxKeyRequestHost).(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
host = l.svcCtx.Config.Host
|
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
|
// Create payment URL for user redirection
|
||||||
@ -317,18 +331,29 @@ func (l *PurchaseCheckoutLogic) CryptoSaaSPayment(config *payment.Payment, info
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// gateway mod
|
||||||
|
isGatewayMod := report.IsGatewayMode()
|
||||||
|
|
||||||
// Build notification URL for payment status callbacks
|
// Build notification URL for payment status callbacks
|
||||||
notifyUrl := ""
|
notifyUrl := ""
|
||||||
if config.Domain != "" {
|
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 {
|
} else {
|
||||||
host, ok := l.ctx.Value(constant.CtxKeyRequestHost).(string)
|
host, ok := l.ctx.Value(constant.CtxKeyRequestHost).(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
host = l.svcCtx.Config.Host
|
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
|
// Create payment URL for user redirection
|
||||||
url := client.CreatePayUrl(epay.Order{
|
url := client.CreatePayUrl(epay.Order{
|
||||||
Name: l.svcCtx.Config.Site.SiteName,
|
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)
|
sub.ResetTime = calculateNextResetTime(&sub)
|
||||||
resp.List = append(resp.List, 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
|
CommissionTypePurchase uint16 = 331 // Purchase
|
||||||
CommissionTypeRenewal uint16 = 332 // Renewal
|
CommissionTypeRenewal uint16 = 332 // Renewal
|
||||||
CommissionTypeRefund uint16 = 333 // Refund
|
CommissionTypeRefund uint16 = 333 // Refund
|
||||||
commissionTypeWithdraw uint16 = 334 // withdraw
|
CommissionTypeWithdraw uint16 = 334 // withdraw
|
||||||
CommissionTypeAdjust uint16 = 335 // Admin Adjust
|
CommissionTypeAdjust uint16 = 335 // Admin Adjust
|
||||||
|
CommissionTypeConvertBalance uint16 = 336 // Convert to Balance
|
||||||
GiftTypeIncrease uint16 = 341 // Increase
|
GiftTypeIncrease uint16 = 341 // Increase
|
||||||
GiftTypeReduce uint16 = 342 // Reduce
|
GiftTypeReduce uint16 = 342 // Reduce
|
||||||
)
|
)
|
||||||
|
|||||||
@ -36,6 +36,7 @@ type SubscribeDetails struct {
|
|||||||
Token string `gorm:"index:idx_token;unique;type:varchar(255);default:'';comment:Token"`
|
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"`
|
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"`
|
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"`
|
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
|
||||||
UpdatedAt time.Time `gorm:"comment:Update 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"`
|
EnableTradeNotify *bool `gorm:"default:false;not null;comment:Enable Trade Notifications"`
|
||||||
AuthMethods []AuthMethods `gorm:"foreignKey:UserId;references:Id"`
|
AuthMethods []AuthMethods `gorm:"foreignKey:UserId;references:Id"`
|
||||||
UserDevices []Device `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"`
|
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
|
||||||
UpdatedAt time.Time `gorm:"comment:Update 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"`
|
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"`
|
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"`
|
||||||
|
Note string `gorm:"type:varchar(500);default:'';comment:User note for subscription"`
|
||||||
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
|
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
|
||||||
UpdatedAt time.Time `gorm:"comment:Update Time"`
|
UpdatedAt time.Time `gorm:"comment:Update Time"`
|
||||||
}
|
}
|
||||||
@ -100,3 +102,18 @@ type DeviceOnlineRecord struct {
|
|||||||
func (DeviceOnlineRecord) TableName() string {
|
func (DeviceOnlineRecord) TableName() string {
|
||||||
return "user_device_online_record"
|
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"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/perfect-panel/server/internal/report"
|
||||||
"github.com/perfect-panel/server/pkg/logger"
|
"github.com/perfect-panel/server/pkg/logger"
|
||||||
|
|
||||||
"github.com/perfect-panel/server/pkg/proc"
|
"github.com/perfect-panel/server/pkg/proc"
|
||||||
@ -48,7 +50,7 @@ func initServer(svc *svc.ServiceContext) *gin.Engine {
|
|||||||
}
|
}
|
||||||
r.Use(sessions.Sessions("ppanel", sessionStore))
|
r.Use(sessions.Sessions("ppanel", sessionStore))
|
||||||
// use cors middleware
|
// 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
|
// register handlers
|
||||||
handler.RegisterHandlers(r, svc)
|
handler.RegisterHandlers(r, svc)
|
||||||
@ -65,9 +67,32 @@ func (m *Service) Start() {
|
|||||||
if m.svc == nil {
|
if m.svc == nil {
|
||||||
panic("config file path is nil")
|
panic("config file path is nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
// init service
|
// init service
|
||||||
r := initServer(m.svc)
|
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{
|
m.server = &http.Server{
|
||||||
Addr: serverAddr,
|
Addr: serverAddr,
|
||||||
Handler: r,
|
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
|
Config config.Config
|
||||||
Queue *asynq.Client
|
Queue *asynq.Client
|
||||||
ExchangeRate float64
|
ExchangeRate float64
|
||||||
|
GeoIP *IPLocation
|
||||||
|
|
||||||
//NodeCache *cache.NodeCacheClient
|
//NodeCache *cache.NodeCacheClient
|
||||||
AuthModel auth.Model
|
AuthModel auth.Model
|
||||||
@ -68,9 +69,17 @@ func NewServiceContext(c config.Config) *ServiceContext {
|
|||||||
db, err := orm.ConnectMysql(orm.Mysql{
|
db, err := orm.ConnectMysql(orm.Mysql{
|
||||||
Config: c.MySQL,
|
Config: c.MySQL,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err.Error())
|
panic(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IP location initialize
|
||||||
|
geoIP, err := NewIPLocation("./cache/GeoLite2-City.mmdb")
|
||||||
|
if err != nil {
|
||||||
|
panic(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
rds := redis.NewClient(&redis.Options{
|
rds := redis.NewClient(&redis.Options{
|
||||||
Addr: c.Redis.Host,
|
Addr: c.Redis.Host,
|
||||||
Password: c.Redis.Pass,
|
Password: c.Redis.Pass,
|
||||||
@ -89,6 +98,7 @@ func NewServiceContext(c config.Config) *ServiceContext {
|
|||||||
Config: c,
|
Config: c,
|
||||||
Queue: NewAsynqClient(c),
|
Queue: NewAsynqClient(c),
|
||||||
ExchangeRate: 1.0,
|
ExchangeRate: 1.0,
|
||||||
|
GeoIP: geoIP,
|
||||||
//NodeCache: cache.NewNodeCacheClient(rds),
|
//NodeCache: cache.NewNodeCacheClient(rds),
|
||||||
AuthLimiter: authLimiter,
|
AuthLimiter: authLimiter,
|
||||||
AdsModel: ads.NewModel(db, rds),
|
AdsModel: ads.NewModel(db, rds),
|
||||||
|
|||||||
@ -239,6 +239,11 @@ type CommissionLog struct {
|
|||||||
Timestamp int64 `json:"timestamp"`
|
Timestamp int64 `json:"timestamp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CommissionWithdrawRequest struct {
|
||||||
|
Amount int64 `json:"amount"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
type ConnectionRecords struct {
|
type ConnectionRecords struct {
|
||||||
CurrentContinuousDays int64 `json:"current_continuous_days"`
|
CurrentContinuousDays int64 `json:"current_continuous_days"`
|
||||||
HistoryContinuousDays int64 `json:"history_continuous_days"`
|
HistoryContinuousDays int64 `json:"history_continuous_days"`
|
||||||
@ -1169,6 +1174,12 @@ type HasMigrateSeverNodeResponse struct {
|
|||||||
HasMigrate bool `json:"has_migrate"`
|
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 {
|
type Hysteria2 struct {
|
||||||
Port int `json:"port" validate:"required"`
|
Port int `json:"port" validate:"required"`
|
||||||
HopPorts string `json:"hop_ports" validate:"required"`
|
HopPorts string `json:"hop_ports" validate:"required"`
|
||||||
@ -1232,6 +1243,12 @@ type MobileAuthenticateConfig struct {
|
|||||||
Whitelist []string `json:"whitelist"`
|
Whitelist []string `json:"whitelist"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ModuleConfig struct {
|
||||||
|
Secret string `json:"secret"` // 通讯密钥
|
||||||
|
ServiceName string `json:"service_name"` // 服务名称
|
||||||
|
ServiceVersion string `json:"service_version"` // 服务版本
|
||||||
|
}
|
||||||
|
|
||||||
type Node struct {
|
type Node struct {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@ -1551,7 +1568,7 @@ type PurchaseOrderResponse struct {
|
|||||||
|
|
||||||
type QueryAnnouncementRequest struct {
|
type QueryAnnouncementRequest struct {
|
||||||
Page int `form:"page"`
|
Page int `form:"page"`
|
||||||
Size int `form:"size,default=15"`
|
Size int `form:"size"`
|
||||||
Pinned *bool `form:"pinned"`
|
Pinned *bool `form:"pinned"`
|
||||||
Popup *bool `form:"popup"`
|
Popup *bool `form:"popup"`
|
||||||
}
|
}
|
||||||
@ -1570,6 +1587,16 @@ type QueryDocumentListResponse struct {
|
|||||||
List []Document `json:"list"`
|
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 {
|
type QueryNodeTagResponse struct {
|
||||||
Tags []string `json:"tags"`
|
Tags []string `json:"tags"`
|
||||||
}
|
}
|
||||||
@ -1712,6 +1739,16 @@ type QueryUserSubscribeNodeListResponse struct {
|
|||||||
List []UserSubscribeInfo `json:"list"`
|
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 {
|
type QuotaTask struct {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
Subscribers []int64 `json:"subscribers"`
|
Subscribers []int64 `json:"subscribers"`
|
||||||
@ -1771,6 +1808,10 @@ type RenewalOrderResponse struct {
|
|||||||
OrderNo string `json:"order_no"`
|
OrderNo string `json:"order_no"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ResetAllSubscribeTokenResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
}
|
||||||
|
|
||||||
type ResetPasswordRequest struct {
|
type ResetPasswordRequest struct {
|
||||||
Identifier string `json:"identifier"`
|
Identifier string `json:"identifier"`
|
||||||
Email string `json:"email" validate:"required"`
|
Email string `json:"email" validate:"required"`
|
||||||
@ -2464,6 +2505,15 @@ type UpdateUserPasswordRequest struct {
|
|||||||
Password string `json:"password" validate:"required"`
|
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 {
|
type UpdateUserSubscribeRequest struct {
|
||||||
UserSubscribeId int64 `json:"user_subscribe_id"`
|
UserSubscribeId int64 `json:"user_subscribe_id"`
|
||||||
SubscribeId int64 `json:"subscribe_id"`
|
SubscribeId int64 `json:"subscribe_id"`
|
||||||
@ -2497,6 +2547,7 @@ type User struct {
|
|||||||
EnableTradeNotify bool `json:"enable_trade_notify"`
|
EnableTradeNotify bool `json:"enable_trade_notify"`
|
||||||
AuthMethods []UserAuthMethod `json:"auth_methods"`
|
AuthMethods []UserAuthMethod `json:"auth_methods"`
|
||||||
UserDevices []UserDevice `json:"user_devices"`
|
UserDevices []UserDevice `json:"user_devices"`
|
||||||
|
Rules []string `json:"rules"`
|
||||||
CreatedAt int64 `json:"created_at"`
|
CreatedAt int64 `json:"created_at"`
|
||||||
UpdatedAt int64 `json:"updated_at"`
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
DeletedAt int64 `json:"deleted_at,omitempty"`
|
DeletedAt int64 `json:"deleted_at,omitempty"`
|
||||||
@ -2587,6 +2638,7 @@ type UserSubscribe struct {
|
|||||||
Upload int64 `json:"upload"`
|
Upload int64 `json:"upload"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
Status uint8 `json:"status"`
|
Status uint8 `json:"status"`
|
||||||
|
Short string `json:"short"`
|
||||||
CreatedAt int64 `json:"created_at"`
|
CreatedAt int64 `json:"created_at"`
|
||||||
UpdatedAt int64 `json:"updated_at"`
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
}
|
}
|
||||||
@ -2751,3 +2803,14 @@ type WeeklyStat struct {
|
|||||||
DayName string `json:"day_name"`
|
DayName string `json:"day_name"`
|
||||||
Hours float64 `json:"hours"`
|
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 (
|
var (
|
||||||
Version = "unknown version"
|
Version = "unknown version"
|
||||||
BuildTime = "unknown time"
|
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
|
TelegramNotBound uint32 = 20007
|
||||||
UserNotBindOauth uint32 = 20008
|
UserNotBindOauth uint32 = 20008
|
||||||
InviteCodeError uint32 = 20009
|
InviteCodeError uint32 = 20009
|
||||||
RegisterIPLimit uint32 = 20010
|
UserCommissionNotEnough uint32 = 20010
|
||||||
|
RegisterIPLimit uint32 = 20011
|
||||||
)
|
)
|
||||||
|
|
||||||
// Node error
|
// Node error
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user