Merge upstream/master into develop

Sync upstream changes from perfect-panel/server

  Includes updates from v1.0.1 to v1.2.5:
  - Currency configuration support
  - Subscribe improvements (short token, inventory check, etc.)
  - Node management enhancements
  - Database migrations
  - Bug fixes and optimizations
This commit is contained in:
EUForest 2026-01-02 12:51:55 +08:00
parent 47c41d1d14
commit 80ee9a6acf
63 changed files with 947 additions and 1509 deletions

View File

@ -8,16 +8,24 @@ import (
)
type Adapter struct {
SiteName string // 站点名称
Servers []*node.Node // 服务器列表
UserInfo User // 用户信息
ClientTemplate string // 客户端配置模板
OutputFormat string // 输出格式,默认是 base64
SubscribeName string // 订阅名称
Type string // 协议类型
SiteName string // 站点名称
Servers []*node.Node // 服务器列表
UserInfo User // 用户信息
ClientTemplate string // 客户端配置模板
OutputFormat string // 输出格式,默认是 base64
SubscribeName string // 订阅名称
Params map[string]string // 其他参数
}
type Option func(*Adapter)
func WithParams(params map[string]string) Option {
return func(opts *Adapter) {
opts.Params = params
}
}
// WithServers 设置服务器列表
func WithServers(servers []*node.Node) Option {
return func(opts *Adapter) {
@ -101,55 +109,58 @@ func (adapter *Adapter) Proxies(servers []*node.Node) ([]Proxy, error) {
}
for _, protocol := range protocols {
if protocol.Type == item.Protocol {
proxies = append(proxies, Proxy{
Sort: item.Sort,
Name: item.Name,
Server: item.Address,
Port: item.Port,
Type: item.Protocol,
Tags: strings.Split(item.Tags, ","),
Security: protocol.Security,
SNI: protocol.SNI,
AllowInsecure: protocol.AllowInsecure,
Fingerprint: protocol.Fingerprint,
RealityServerAddr: protocol.RealityServerAddr,
RealityServerPort: protocol.RealityServerPort,
RealityPrivateKey: protocol.RealityPrivateKey,
RealityPublicKey: protocol.RealityPublicKey,
RealityShortId: protocol.RealityShortId,
Transport: protocol.Transport,
Host: protocol.Host,
Path: protocol.Path,
ServiceName: protocol.ServiceName,
Method: protocol.Cipher,
ServerKey: protocol.ServerKey,
Flow: protocol.Flow,
HopPorts: protocol.HopPorts,
HopInterval: protocol.HopInterval,
ObfsPassword: protocol.ObfsPassword,
UpMbps: protocol.UpMbps,
DownMbps: protocol.DownMbps,
DisableSNI: protocol.DisableSNI,
ReduceRtt: protocol.ReduceRtt,
UDPRelayMode: protocol.UDPRelayMode,
CongestionController: protocol.CongestionController,
PaddingScheme: protocol.PaddingScheme,
Multiplex: protocol.Multiplex,
XhttpMode: protocol.XhttpMode,
XhttpExtra: protocol.XhttpExtra,
Encryption: protocol.Encryption,
EncryptionMode: protocol.EncryptionMode,
EncryptionRtt: protocol.EncryptionRtt,
EncryptionTicket: protocol.EncryptionTicket,
EncryptionServerPadding: protocol.EncryptionServerPadding,
EncryptionPrivateKey: protocol.EncryptionPrivateKey,
EncryptionClientPadding: protocol.EncryptionClientPadding,
EncryptionPassword: protocol.EncryptionPassword,
Ratio: protocol.Ratio,
CertMode: protocol.CertMode,
CertDNSProvider: protocol.CertDNSProvider,
CertDNSEnv: protocol.CertDNSEnv,
})
proxies = append(
proxies,
Proxy{
Sort: item.Sort,
Name: item.Name,
Server: item.Address,
Port: item.Port,
Type: item.Protocol,
Tags: strings.Split(item.Tags, ","),
Security: protocol.Security,
SNI: protocol.SNI,
AllowInsecure: protocol.AllowInsecure,
Fingerprint: protocol.Fingerprint,
RealityServerAddr: protocol.RealityServerAddr,
RealityServerPort: protocol.RealityServerPort,
RealityPrivateKey: protocol.RealityPrivateKey,
RealityPublicKey: protocol.RealityPublicKey,
RealityShortId: protocol.RealityShortId,
Transport: protocol.Transport,
Host: protocol.Host,
Path: protocol.Path,
ServiceName: protocol.ServiceName,
Method: protocol.Cipher,
ServerKey: protocol.ServerKey,
Flow: protocol.Flow,
HopPorts: protocol.HopPorts,
HopInterval: protocol.HopInterval,
ObfsPassword: protocol.ObfsPassword,
UpMbps: protocol.UpMbps,
DownMbps: protocol.DownMbps,
DisableSNI: protocol.DisableSNI,
ReduceRtt: protocol.ReduceRtt,
UDPRelayMode: protocol.UDPRelayMode,
CongestionController: protocol.CongestionController,
PaddingScheme: protocol.PaddingScheme,
Multiplex: protocol.Multiplex,
XhttpMode: protocol.XhttpMode,
XhttpExtra: protocol.XhttpExtra,
Encryption: protocol.Encryption,
EncryptionMode: protocol.EncryptionMode,
EncryptionRtt: protocol.EncryptionRtt,
EncryptionTicket: protocol.EncryptionTicket,
EncryptionServerPadding: protocol.EncryptionServerPadding,
EncryptionPrivateKey: protocol.EncryptionPrivateKey,
EncryptionClientPadding: protocol.EncryptionClientPadding,
EncryptionPassword: protocol.EncryptionPassword,
Ratio: protocol.Ratio,
CertMode: protocol.CertMode,
CertDNSProvider: protocol.CertDNSProvider,
CertDNSEnv: protocol.CertDNSEnv,
},
)
}
}
}

View File

@ -93,12 +93,13 @@ type User struct {
}
type Client struct {
SiteName string // Name of the site
SubscribeName string // Name of the subscription
ClientTemplate string // Template for the entire client configuration
OutputFormat string // json, yaml, etc.
Proxies []Proxy // List of proxy configurations
UserInfo User // User information
SiteName string // Name of the site
SubscribeName string // Name of the subscription
ClientTemplate string // Template for the entire client configuration
OutputFormat string // json, yaml, etc.
Proxies []Proxy // List of proxy configurations
UserInfo User // User information
Params map[string]string // Additional parameters
}
func (c *Client) Build() ([]byte, error) {
@ -119,6 +120,7 @@ func (c *Client) Build() ([]byte, error) {
"OutputFormat": c.OutputFormat,
"Proxies": proxies,
"UserInfo": c.UserInfo,
"Params": c.Params,
})
if err != nil {
return nil, err

View File

@ -189,14 +189,6 @@ service ppanel {
@handler ToggleNodeStatus
post /node/status/toggle (ToggleNodeStatusRequest)
@doc "Check if there is any server or node to migrate"
@handler HasMigrateSeverNode
get /migrate/has returns (HasMigrateSeverNodeResponse)
@doc "Migrate server and node data to new database"
@handler MigrateServerNode
post /migrate/run returns (MigrateServerNodeResponse)
@doc "Reset server sort"
@handler ResetSortWithServer
post /server/sort (ResetSortRequest)

View File

@ -34,50 +34,52 @@ type (
Ids []int64 `json:"ids" validate:"required"`
}
CreateSubscribeRequest {
Name string `json:"name" validate:"required"`
Language string `json:"language"`
Description string `json:"description"`
UnitPrice int64 `json:"unit_price"`
UnitTime string `json:"unit_time"`
Discount []SubscribeDiscount `json:"discount"`
Replacement int64 `json:"replacement"`
Inventory int64 `json:"inventory"`
Traffic int64 `json:"traffic"`
SpeedLimit int64 `json:"speed_limit"`
DeviceLimit int64 `json:"device_limit"`
Quota int64 `json:"quota"`
Nodes []int64 `json:"nodes"`
NodeTags []string `json:"node_tags"`
Show *bool `json:"show"`
Sell *bool `json:"sell"`
DeductionRatio int64 `json:"deduction_ratio"`
AllowDeduction *bool `json:"allow_deduction"`
ResetCycle int64 `json:"reset_cycle"`
RenewalReset *bool `json:"renewal_reset"`
Name string `json:"name" validate:"required"`
Language string `json:"language"`
Description string `json:"description"`
UnitPrice int64 `json:"unit_price"`
UnitTime string `json:"unit_time"`
Discount []SubscribeDiscount `json:"discount"`
Replacement int64 `json:"replacement"`
Inventory int64 `json:"inventory"`
Traffic int64 `json:"traffic"`
SpeedLimit int64 `json:"speed_limit"`
DeviceLimit int64 `json:"device_limit"`
Quota int64 `json:"quota"`
Nodes []int64 `json:"nodes"`
NodeTags []string `json:"node_tags"`
Show *bool `json:"show"`
Sell *bool `json:"sell"`
DeductionRatio int64 `json:"deduction_ratio"`
AllowDeduction *bool `json:"allow_deduction"`
ResetCycle int64 `json:"reset_cycle"`
RenewalReset *bool `json:"renewal_reset"`
ShowOriginalPrice bool `json:"show_original_price"`
}
UpdateSubscribeRequest {
Id int64 `json:"id" validate:"required"`
Name string `json:"name" validate:"required"`
Language string `json:"language"`
Description string `json:"description"`
UnitPrice int64 `json:"unit_price"`
UnitTime string `json:"unit_time"`
Discount []SubscribeDiscount `json:"discount"`
Replacement int64 `json:"replacement"`
Inventory int64 `json:"inventory"`
Traffic int64 `json:"traffic"`
SpeedLimit int64 `json:"speed_limit"`
DeviceLimit int64 `json:"device_limit"`
Quota int64 `json:"quota"`
Nodes []int64 `json:"nodes"`
NodeTags []string `json:"node_tags"`
Show *bool `json:"show"`
Sell *bool `json:"sell"`
Sort int64 `json:"sort"`
DeductionRatio int64 `json:"deduction_ratio"`
AllowDeduction *bool `json:"allow_deduction"`
ResetCycle int64 `json:"reset_cycle"`
RenewalReset *bool `json:"renewal_reset"`
Id int64 `json:"id" validate:"required"`
Name string `json:"name" validate:"required"`
Language string `json:"language"`
Description string `json:"description"`
UnitPrice int64 `json:"unit_price"`
UnitTime string `json:"unit_time"`
Discount []SubscribeDiscount `json:"discount"`
Replacement int64 `json:"replacement"`
Inventory int64 `json:"inventory"`
Traffic int64 `json:"traffic"`
SpeedLimit int64 `json:"speed_limit"`
DeviceLimit int64 `json:"device_limit"`
Quota int64 `json:"quota"`
Nodes []int64 `json:"nodes"`
NodeTags []string `json:"node_tags"`
Show *bool `json:"show"`
Sell *bool `json:"sell"`
Sort int64 `json:"sort"`
DeductionRatio int64 `json:"deduction_ratio"`
AllowDeduction *bool `json:"allow_deduction"`
ResetCycle int64 `json:"reset_cycle"`
RenewalReset *bool `json:"renewal_reset"`
ShowOriginalPrice bool `json:"show_original_price"`
}
SubscribeSortRequest {
Sort []SortItem `json:"sort"`

View File

@ -19,6 +19,7 @@ type (
Size int `form:"size"`
Search string `form:"search,omitempty"`
UserId *int64 `form:"user_id,omitempty"`
Unscoped bool `form:"unscoped,omitempty"`
SubscribeId *int64 `form:"subscribe_id,omitempty"`
UserSubscribeId *int64 `form:"user_subscribe_id,omitempty"`
}
@ -183,6 +184,12 @@ type (
GetUserSubscribeByIdRequest {
Id int64 `form:"id" validate:"required"`
}
ToggleUserSubscribeStatusRequest {
UserSubscribeId int64 `json:"user_subscribe_id"`
}
ResetUserSubscribeTrafficRequest {
UserSubscribeId int64 `json:"user_subscribe_id"`
}
)
@server (
@ -291,5 +298,17 @@ service ppanel {
@doc "Get user login logs"
@handler GetUserLoginLogs
get /login/logs (GetUserLoginLogsRequest) returns (GetUserLoginLogsResponse)
@doc "Reset user subscribe token"
@handler ResetUserSubscribeToken
post /subscribe/reset/token (ResetUserSubscribeTokenRequest)
@doc "Stop user subscribe"
@handler ToggleUserSubscribeStatus
post /subscribe/toggle (ToggleUserSubscribeStatusRequest)
@doc "Reset user subscribe traffic"
@handler ResetUserSubscribeTraffic
post /subscribe/reset/traffic (ResetUserSubscribeTrafficRequest)
}

View File

@ -14,9 +14,11 @@ type (
QuerySubscribeListRequest {
Language string `form:"language"`
}
QueryUserSubscribeNodeListResponse {
List []UserSubscribeInfo `json:"list"`
}
QueryUserSubscribeNodeListResponse {
List []UserSubscribeInfo `json:"list"`
}
UserSubscribeInfo {
Id int64 `json:"id"`
UserId int64 `json:"user_id"`

View File

@ -66,9 +66,7 @@ type (
UnbindOAuthRequest {
Method string `json:"method"`
}
ResetUserSubscribeTokenRequest {
UserSubscribeId int64 `json:"user_subscribe_id"`
}
GetLoginLogRequest {
Page int `form:"page"`
Size int `form:"size"`
@ -282,15 +280,3 @@ service ppanel {
get /withdrawal_log (QueryWithdrawalLogListRequest) returns (QueryWithdrawalLogListResponse)
}
@server(
prefix: v1/public/user
group: public/user/ws
middleware: AuthMiddleware
)
service ppanel {
@doc "Webosocket Device Connect"
@handler DeviceWsConnect
get /device_ws_connect
}

View File

@ -32,7 +32,6 @@ type (
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
DeletedAt int64 `json:"deleted_at,omitempty"`
IsDel bool `json:"is_del,omitempty"`
}
Follow {
Id int64 `json:"id"`
@ -136,12 +135,14 @@ type (
EnableDomainSuffix bool `json:"enable_domain_suffix"`
DomainSuffixList string `json:"domain_suffix_list"`
}
DeviceAuthticateConfig {
Enable bool `json:"enable"`
ShowAds bool `json:"show_ads"`
EnableSecurity bool `json:"enable_security"`
OnlyRealDevice bool `json:"only_real_device"`
}
DeviceAuthticateConfig {
Enable bool `json:"enable"`
ShowAds bool `json:"show_ads"`
EnableSecurity bool `json:"enable_security"`
OnlyRealDevice bool `json:"only_real_device"`
}
RegisterConfig {
StopRegister bool `json:"stop_register"`
EnableTrial bool `json:"enable_trial"`
@ -209,30 +210,31 @@ type (
Discount float64 `json:"discount"`
}
Subscribe {
Id int64 `json:"id"`
Name string `json:"name"`
Language string `json:"language"`
Description string `json:"description"`
UnitPrice int64 `json:"unit_price"`
UnitTime string `json:"unit_time"`
Discount []SubscribeDiscount `json:"discount"`
Replacement int64 `json:"replacement"`
Inventory int64 `json:"inventory"`
Traffic int64 `json:"traffic"`
SpeedLimit int64 `json:"speed_limit"`
DeviceLimit int64 `json:"device_limit"`
Quota int64 `json:"quota"`
Nodes []int64 `json:"nodes"`
NodeTags []string `json:"node_tags"`
Show bool `json:"show"`
Sell bool `json:"sell"`
Sort int64 `json:"sort"`
DeductionRatio int64 `json:"deduction_ratio"`
AllowDeduction bool `json:"allow_deduction"`
ResetCycle int64 `json:"reset_cycle"`
RenewalReset bool `json:"renewal_reset"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
Id int64 `json:"id"`
Name string `json:"name"`
Language string `json:"language"`
Description string `json:"description"`
UnitPrice int64 `json:"unit_price"`
UnitTime string `json:"unit_time"`
Discount []SubscribeDiscount `json:"discount"`
Replacement int64 `json:"replacement"`
Inventory int64 `json:"inventory"`
Traffic int64 `json:"traffic"`
SpeedLimit int64 `json:"speed_limit"`
DeviceLimit int64 `json:"device_limit"`
Quota int64 `json:"quota"`
Nodes []int64 `json:"nodes"`
NodeTags []string `json:"node_tags"`
Show bool `json:"show"`
Sell bool `json:"sell"`
Sort int64 `json:"sort"`
DeductionRatio int64 `json:"deduction_ratio"`
AllowDeduction bool `json:"allow_deduction"`
ResetCycle int64 `json:"reset_cycle"`
RenewalReset bool `json:"renewal_reset"`
ShowOriginalPrice bool `json:"show_original_price"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
}
SubscribeGroup {
Id int64 `json:"id"`
@ -656,7 +658,7 @@ type (
// public announcement
QueryAnnouncementRequest {
Page int `form:"page"`
Size int `form:"size"`
Size int `form:"size,default=15"`
Pinned *bool `form:"pinned"`
Popup *bool `form:"popup"`
}
@ -673,6 +675,7 @@ type (
List []SubscribeGroup `json:"list"`
Total int64 `json:"total"`
}
GetUserSubscribeTrafficLogsRequest {
Page int `form:"page"`
Size int `form:"size"`
@ -845,5 +848,9 @@ type (
CertDNSProvider string `json:"cert_dns_provider,omitempty"` // DNS provider for certificate
CertDNSEnv string `json:"cert_dns_env,omitempty"` // Environment for DNS provider
}
// reset user subscribe token
ResetUserSubscribeTokenRequest {
UserSubscribeId int64 `json:"user_subscribe_id"`
}
)

2
go.mod
View File

@ -27,7 +27,7 @@ require (
github.com/jinzhu/copier v0.4.0
github.com/klauspost/compress v1.17.7
github.com/nyaruka/phonenumbers v1.5.0
github.com/pkg/errors v0.9.1
github.com/pkg/errors v0.9.1
github.com/redis/go-redis/v9 v9.7.2
github.com/smartwalle/alipay/v3 v3.2.23
github.com/spf13/cast v1.7.0 // indirect

34
initialize/currency.go Normal file
View File

@ -0,0 +1,34 @@
package initialize
import (
"context"
"fmt"
"github.com/perfect-panel/server/internal/config"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/tool"
)
func Currency(ctx *svc.ServiceContext) {
// Retrieve system currency configuration
currency, err := ctx.SystemModel.GetCurrencyConfig(context.Background())
if err != nil {
logger.Errorf("[INIT] Failed to get currency configuration: %v", err.Error())
panic(fmt.Sprintf("[INIT] Failed to get currency configuration: %v", err.Error()))
}
// Parse currency configuration
configs := struct {
CurrencyUnit string
CurrencySymbol string
AccessKey string
}{}
tool.SystemConfigSliceReflectToStruct(currency, &configs)
ctx.Config.Currency = config.Currency{
Unit: configs.CurrencyUnit,
Symbol: configs.CurrencySymbol,
AccessKey: configs.AccessKey,
}
logger.Infof("[INIT] Currency configuration: %v", ctx.Config.Currency)
}

View File

@ -15,6 +15,7 @@ func StartInitSystemConfig(svc *svc.ServiceContext) {
Subscribe(svc)
Register(svc)
Mobile(svc)
Currency(svc)
if !svc.Config.Debug {
Telegram(svc)
}

View File

@ -0,0 +1,2 @@
ALTER TABLE `subscribe`
DROP COLUMN `show_original_price`;

View File

@ -0,0 +1,2 @@
ALTER TABLE `subscribe`
ADD COLUMN `show_original_price` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'display the original price: 0 not display, 1 display' AFTER `created_at`;

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS `server_group`;

View File

@ -0,0 +1,5 @@
-- This migration script reverts the inventory values in the 'subscribe' table
UPDATE `subscribe`
SET `inventory` = 0
WHERE `inventory` = -1;

View File

@ -0,0 +1,4 @@
-- Update the `subscribe` table to set `inventory` to -1 where it is currently 0
UPDATE `subscribe`
SET `inventory` = -1
WHERE `inventory` = 0;

View File

@ -29,6 +29,7 @@ type Config struct {
Invite InviteConfig `yaml:"Invite"`
Telegram Telegram `yaml:"Telegram"`
Log Log `yaml:"Log"`
Currency Currency `yaml:"Currency"`
Administrator struct {
Email string `yaml:"Email" default:"admin@ppanel.dev"`
Password string `yaml:"Password" default:"password"`
@ -241,3 +242,9 @@ type NodeDBConfig struct {
Block string
Outbound string
}
type Currency struct {
Unit string `yaml:"Unit" default:"CNY"`
Symbol string `yaml:"Symbol" default:"USD"`
AccessKey string `yaml:"AccessKey" default:""`
}

View File

@ -1,18 +0,0 @@
package server
import (
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/admin/server"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/pkg/result"
)
// Check if there is any server or node to migrate
func HasMigrateSeverNodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
l := server.NewHasMigrateSeverNodeLogic(c.Request.Context(), svcCtx)
resp, err := l.HasMigrateSeverNode()
result.HttpResult(c, resp, err)
}
}

View File

@ -1,18 +0,0 @@
package server
import (
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/admin/server"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/pkg/result"
)
// Migrate server and node data to new database
func MigrateServerNodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
l := server.NewMigrateServerNodeLogic(c.Request.Context(), svcCtx)
resp, err := l.MigrateServerNode()
result.HttpResult(c, resp, err)
}
}

View File

@ -0,0 +1,26 @@
package user
import (
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/admin/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/result"
)
// Reset user subscribe token
func ResetUserSubscribeTokenHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
var req types.ResetUserSubscribeTokenRequest
_ = c.ShouldBind(&req)
validateErr := svcCtx.Validate(&req)
if validateErr != nil {
result.ParamErrorResult(c, validateErr)
return
}
l := user.NewResetUserSubscribeTokenLogic(c.Request.Context(), svcCtx)
err := l.ResetUserSubscribeToken(&req)
result.HttpResult(c, nil, err)
}
}

View File

@ -0,0 +1,26 @@
package user
import (
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/admin/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/result"
)
// Reset user subscribe traffic
func ResetUserSubscribeTrafficHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
var req types.ResetUserSubscribeTrafficRequest
_ = c.ShouldBind(&req)
validateErr := svcCtx.Validate(&req)
if validateErr != nil {
result.ParamErrorResult(c, validateErr)
return
}
l := user.NewResetUserSubscribeTrafficLogic(c.Request.Context(), svcCtx)
err := l.ResetUserSubscribeTraffic(&req)
result.HttpResult(c, nil, err)
}
}

View File

@ -0,0 +1,26 @@
package user
import (
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/admin/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/result"
)
// Stop user subscribe
func ToggleUserSubscribeStatusHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
var req types.ToggleUserSubscribeStatusRequest
_ = c.ShouldBind(&req)
validateErr := svcCtx.Validate(&req)
if validateErr != nil {
result.ParamErrorResult(c, validateErr)
return
}
l := user.NewToggleUserSubscribeStatusLogic(c.Request.Context(), svcCtx)
err := l.ToggleUserSubscribeStatus(&req)
result.HttpResult(c, nil, err)
}
}

View File

@ -33,7 +33,6 @@ import (
publicSubscribe "github.com/perfect-panel/server/internal/handler/public/subscribe"
publicTicket "github.com/perfect-panel/server/internal/handler/public/ticket"
publicUser "github.com/perfect-panel/server/internal/handler/public/user"
publicUserWs "github.com/perfect-panel/server/internal/handler/public/user/ws"
server "github.com/perfect-panel/server/internal/handler/server"
"github.com/perfect-panel/server/internal/middleware"
"github.com/perfect-panel/server/internal/svc"
@ -312,12 +311,6 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
// Filter Server List
adminServerGroupRouter.GET("/list", adminServer.FilterServerListHandler(serverCtx))
// Check if there is any server or node to migrate
adminServerGroupRouter.GET("/migrate/has", adminServer.HasMigrateSeverNodeHandler(serverCtx))
// Migrate server and node data to new database
adminServerGroupRouter.POST("/migrate/run", adminServer.MigrateServerNodeHandler(serverCtx))
// Create Node
adminServerGroupRouter.POST("/node/create", adminServer.CreateNodeHandler(serverCtx))
@ -583,6 +576,15 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
// Get user subcribe reset traffic logs
adminUserGroupRouter.GET("/subscribe/reset/logs", adminUser.GetUserSubscribeResetTrafficLogsHandler(serverCtx))
// Reset user subscribe token
adminUserGroupRouter.POST("/subscribe/reset/token", adminUser.ResetUserSubscribeTokenHandler(serverCtx))
// Reset user subscribe traffic
adminUserGroupRouter.POST("/subscribe/reset/traffic", adminUser.ResetUserSubscribeTrafficHandler(serverCtx))
// Stop user subscribe
adminUserGroupRouter.POST("/subscribe/toggle", adminUser.ToggleUserSubscribeStatusHandler(serverCtx))
// Get user subcribe traffic logs
adminUserGroupRouter.GET("/subscribe/traffic_logs", adminUser.GetUserSubscribeTrafficLogsHandler(serverCtx))
}
@ -872,14 +874,6 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
publicUserGroupRouter.GET("/withdrawal_log", publicUser.QueryWithdrawalLogHandler(serverCtx))
}
publicUserWsGroupRouter := router.Group("/v1/public/user")
publicUserWsGroupRouter.Use(middleware.AuthMiddleware(serverCtx))
{
// Webosocket Device Connect
publicUserWsGroupRouter.GET("/device_ws_connect", publicUserWs.DeviceWsConnectHandler(serverCtx))
}
serverGroupRouter := router.Group("/v1/server")
serverGroupRouter.Use(middleware.ServerMiddleware(serverCtx))

View File

@ -23,6 +23,10 @@ func SubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
ua := c.GetHeader("User-Agent")
req.UA = c.Request.Header.Get("User-Agent")
req.Flag = c.Query("flag")
req.Type = c.Query("type")
// 获取所有查询参数
req.Params = getQueryMap(c.Request)
if svcCtx.Config.Subscribe.PanDomain {
domain := c.Request.Host
domainArr := strings.Split(domain, ".")
@ -33,7 +37,8 @@ func SubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
c.Abort()
return
}
if short != domainArr[0] {
if strings.ToLower(short) != strings.ToLower(domainArr[0]) {
logger.Debugf("[SubscribeHandler] Generate short token failed, short: %s, domain: %s", short, domainArr[0])
c.String(http.StatusForbidden, "Access denied")
c.Abort()
return
@ -94,3 +99,14 @@ func RegisterSubscribeHandlers(router *gin.Engine, serverCtx *svc.ServiceContext
}
router.GET(path, SubscribeHandler(serverCtx))
}
// GetQueryMap 将 http.Request 的查询参数转换为 map[string]string
func getQueryMap(r *http.Request) map[string]string {
result := make(map[string]string)
for k, v := range r.URL.Query() {
if len(v) > 0 {
result[k] = v[0]
}
}
return result
}

View File

@ -31,6 +31,7 @@ func (l *CreateNodeLogic) CreateNode(req *types.CreateNodeRequest) error {
data := node.Node{
Name: req.Name,
Tags: tool.StringSliceToString(req.Tags),
Enabled: req.Enabled,
Port: req.Port,
Address: req.Address,
ServerId: req.ServerId,

View File

@ -1,52 +0,0 @@
package server
import (
"context"
"github.com/perfect-panel/server/internal/model/node"
"github.com/perfect-panel/server/internal/model/server"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
)
type HasMigrateSeverNodeLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// NewHasMigrateSeverNodeLogic Check if there is any server or node to migrate
func NewHasMigrateSeverNodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *HasMigrateSeverNodeLogic {
return &HasMigrateSeverNodeLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *HasMigrateSeverNodeLogic) HasMigrateSeverNode() (resp *types.HasMigrateSeverNodeResponse, err error) {
var oldCount, newCount int64
query := l.svcCtx.DB.WithContext(l.ctx)
err = query.Model(&server.Server{}).Count(&oldCount).Error
if err != nil {
l.Errorw("[HasMigrateSeverNode] Query Old Server Count Error: ", logger.Field("error", err.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "[HasMigrateSeverNode] Query Old Server Count Error")
}
err = query.Model(&node.Server{}).Count(&newCount).Error
if err != nil {
l.Errorw("[HasMigrateSeverNode] Query New Server Count Error: ", logger.Field("error", err.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "[HasMigrateSeverNode] Query New Server Count Error")
}
var shouldMigrate bool
if oldCount != 0 && newCount == 0 {
shouldMigrate = true
}
return &types.HasMigrateSeverNodeResponse{
HasMigrate: shouldMigrate,
}, nil
}

View File

@ -1,338 +0,0 @@
package server
import (
"context"
"encoding/json"
"fmt"
"github.com/perfect-panel/server/internal/model/node"
"github.com/perfect-panel/server/internal/model/server"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
)
type MigrateServerNodeLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// NewMigrateServerNodeLogic Migrate server and node data to new database
func NewMigrateServerNodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *MigrateServerNodeLogic {
return &MigrateServerNodeLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *MigrateServerNodeLogic) MigrateServerNode() (resp *types.MigrateServerNodeResponse, err error) {
tx := l.svcCtx.DB.WithContext(l.ctx).Begin()
var oldServers []*server.Server
var newServers []*node.Server
var newNodes []*node.Node
err = tx.Model(&server.Server{}).Find(&oldServers).Error
if err != nil {
l.Errorw("[MigrateServerNode] Query Old Server List Error: ", logger.Field("error", err.Error()))
return &types.MigrateServerNodeResponse{
Succee: 0,
Fail: 0,
Message: fmt.Sprintf("Query Old Server List Error: %s", err.Error()),
}, nil
}
for _, oldServer := range oldServers {
data, err := l.adapterServer(oldServer)
if err != nil {
l.Errorw("[MigrateServerNode] Adapter Server Error: ", logger.Field("error", err.Error()))
if resp == nil {
resp = &types.MigrateServerNodeResponse{}
}
resp.Fail++
if resp.Message == "" {
resp.Message = fmt.Sprintf("Adapter Server Error: %s", err.Error())
} else {
resp.Message = fmt.Sprintf("%s; Adapter Server Error: %s", resp.Message, err.Error())
}
continue
}
newServers = append(newServers, data)
newNode, err := l.adapterNode(oldServer)
if err != nil {
l.Errorw("[MigrateServerNode] Adapter Node Error: ", logger.Field("error", err.Error()))
if resp == nil {
resp = &types.MigrateServerNodeResponse{}
}
resp.Fail++
if resp.Message == "" {
resp.Message = fmt.Sprintf("Adapter Node Error: %s", err.Error())
} else {
resp.Message = fmt.Sprintf("%s; Adapter Node Error: %s", resp.Message, err.Error())
}
continue
}
for _, item := range newNode {
if item.Port == 0 {
protocols, _ := data.UnmarshalProtocols()
if len(protocols) > 0 {
item.Port = protocols[0].Port
}
}
newNodes = append(newNodes, item)
}
}
if len(newServers) > 0 {
err = tx.Model(&node.Server{}).CreateInBatches(newServers, 20).Error
if err != nil {
tx.Rollback()
l.Errorw("[MigrateServerNode] Insert New Server List Error: ", logger.Field("error", err.Error()))
return &types.MigrateServerNodeResponse{
Succee: 0,
Fail: uint64(len(newServers)),
Message: fmt.Sprintf("Insert New Server List Error: %s", err.Error()),
}, nil
}
}
if len(newNodes) > 0 {
err = tx.Model(&node.Node{}).CreateInBatches(newNodes, 20).Error
if err != nil {
tx.Rollback()
l.Errorw("[MigrateServerNode] Insert New Node List Error: ", logger.Field("error", err.Error()))
return &types.MigrateServerNodeResponse{
Succee: uint64(len(newServers)),
Fail: uint64(len(newNodes)),
Message: fmt.Sprintf("Insert New Node List Error: %s", err.Error()),
}, nil
}
}
tx.Commit()
return &types.MigrateServerNodeResponse{
Succee: uint64(len(newServers)),
Fail: 0,
Message: fmt.Sprintf("Migrate Success: %d servers and %d nodes", len(newServers), len(newNodes)),
}, nil
}
func (l *MigrateServerNodeLogic) adapterServer(info *server.Server) (*node.Server, error) {
result := &node.Server{
Id: info.Id,
Name: info.Name,
Country: info.Country,
City: info.City,
//Ratio: info.TrafficRatio,
Address: info.ServerAddr,
Sort: int(info.Sort),
Protocols: "",
}
var protocols []node.Protocol
switch info.Protocol {
case ShadowSocks:
var src server.Shadowsocks
err := json.Unmarshal([]byte(info.Config), &src)
if err != nil {
return nil, err
}
protocols = append(protocols, node.Protocol{
Type: "shadowsocks",
Cipher: src.Method,
Port: uint16(src.Port),
ServerKey: src.ServerKey,
Ratio: float64(info.TrafficRatio),
})
case Vmess:
var src server.Vmess
err := json.Unmarshal([]byte(info.Config), &src)
if err != nil {
return nil, err
}
protocol := node.Protocol{
Type: "vmess",
Port: uint16(src.Port),
Security: src.Security,
SNI: src.SecurityConfig.SNI,
AllowInsecure: src.SecurityConfig.AllowInsecure,
Fingerprint: src.SecurityConfig.Fingerprint,
RealityServerAddr: src.SecurityConfig.RealityServerAddr,
RealityServerPort: src.SecurityConfig.RealityServerPort,
RealityPrivateKey: src.SecurityConfig.RealityPrivateKey,
RealityPublicKey: src.SecurityConfig.RealityPublicKey,
RealityShortId: src.SecurityConfig.RealityShortId,
Transport: src.Transport,
Host: src.TransportConfig.Host,
Path: src.TransportConfig.Path,
ServiceName: src.TransportConfig.ServiceName,
Flow: src.Flow,
Ratio: float64(info.TrafficRatio),
}
protocols = append(protocols, protocol)
protocols = append(protocols, protocol)
case Vless:
var src server.Vless
err := json.Unmarshal([]byte(info.Config), &src)
if err != nil {
return nil, err
}
protocol := node.Protocol{
Type: "vless",
Port: uint16(src.Port),
Security: src.Security,
SNI: src.SecurityConfig.SNI,
AllowInsecure: src.SecurityConfig.AllowInsecure,
Fingerprint: src.SecurityConfig.Fingerprint,
RealityServerAddr: src.SecurityConfig.RealityServerAddr,
RealityServerPort: src.SecurityConfig.RealityServerPort,
RealityPrivateKey: src.SecurityConfig.RealityPrivateKey,
RealityPublicKey: src.SecurityConfig.RealityPublicKey,
RealityShortId: src.SecurityConfig.RealityShortId,
Transport: src.Transport,
Host: src.TransportConfig.Host,
Path: src.TransportConfig.Path,
ServiceName: src.TransportConfig.ServiceName,
Flow: src.Flow,
Ratio: float64(info.TrafficRatio),
}
protocols = append(protocols, protocol)
case Trojan:
var src server.Trojan
err := json.Unmarshal([]byte(info.Config), &src)
if err != nil {
return nil, err
}
protocol := node.Protocol{
Type: "trojan",
Port: uint16(src.Port),
Security: src.Security,
SNI: src.SecurityConfig.SNI,
AllowInsecure: src.SecurityConfig.AllowInsecure,
Fingerprint: src.SecurityConfig.Fingerprint,
RealityServerAddr: src.SecurityConfig.RealityServerAddr,
RealityServerPort: src.SecurityConfig.RealityServerPort,
RealityPrivateKey: src.SecurityConfig.RealityPrivateKey,
RealityPublicKey: src.SecurityConfig.RealityPublicKey,
RealityShortId: src.SecurityConfig.RealityShortId,
Transport: src.Transport,
Host: src.TransportConfig.Host,
Path: src.TransportConfig.Path,
ServiceName: src.TransportConfig.ServiceName,
Flow: src.Flow,
Ratio: float64(info.TrafficRatio),
}
protocols = append(protocols, protocol)
case Hysteria2:
var src server.Hysteria2
err := json.Unmarshal([]byte(info.Config), &src)
if err != nil {
return nil, err
}
protocol := node.Protocol{
Type: "hysteria",
Port: uint16(src.Port),
HopPorts: src.HopPorts,
HopInterval: src.HopInterval,
ObfsPassword: src.ObfsPassword,
SNI: src.SecurityConfig.SNI,
AllowInsecure: src.SecurityConfig.AllowInsecure,
Fingerprint: src.SecurityConfig.Fingerprint,
RealityServerAddr: src.SecurityConfig.RealityServerAddr,
RealityServerPort: src.SecurityConfig.RealityServerPort,
RealityPrivateKey: src.SecurityConfig.RealityPrivateKey,
RealityPublicKey: src.SecurityConfig.RealityPublicKey,
RealityShortId: src.SecurityConfig.RealityShortId,
Ratio: float64(info.TrafficRatio),
}
protocols = append(protocols, protocol)
case Tuic:
var src server.Tuic
err := json.Unmarshal([]byte(info.Config), &src)
if err != nil {
return nil, err
}
protocol := node.Protocol{
Type: "tuic",
Port: uint16(src.Port),
DisableSNI: src.DisableSNI,
ReduceRtt: src.ReduceRtt,
UDPRelayMode: src.UDPRelayMode,
CongestionController: src.CongestionController,
SNI: src.SecurityConfig.SNI,
AllowInsecure: src.SecurityConfig.AllowInsecure,
Fingerprint: src.SecurityConfig.Fingerprint,
RealityServerAddr: src.SecurityConfig.RealityServerAddr,
RealityServerPort: src.SecurityConfig.RealityServerPort,
RealityPrivateKey: src.SecurityConfig.RealityPrivateKey,
RealityPublicKey: src.SecurityConfig.RealityPublicKey,
RealityShortId: src.SecurityConfig.RealityShortId,
Ratio: float64(info.TrafficRatio),
}
protocols = append(protocols, protocol)
case AnyTLS:
var src server.AnyTLS
err := json.Unmarshal([]byte(info.Config), &src)
if err != nil {
return nil, err
}
protocol := node.Protocol{
Type: "anytls",
Port: uint16(src.Port),
SNI: src.SecurityConfig.SNI,
AllowInsecure: src.SecurityConfig.AllowInsecure,
Fingerprint: src.SecurityConfig.Fingerprint,
RealityServerAddr: src.SecurityConfig.RealityServerAddr,
RealityServerPort: src.SecurityConfig.RealityServerPort,
RealityPrivateKey: src.SecurityConfig.RealityPrivateKey,
RealityPublicKey: src.SecurityConfig.RealityPublicKey,
RealityShortId: src.SecurityConfig.RealityShortId,
Ratio: float64(info.TrafficRatio),
}
protocols = append(protocols, protocol)
}
if len(protocols) > 0 {
err := result.MarshalProtocols(protocols)
if err != nil {
return nil, err
}
}
return result, nil
}
func (l *MigrateServerNodeLogic) adapterNode(info *server.Server) ([]*node.Node, error) {
var nodes []*node.Node
enable := true
switch info.RelayMode {
case server.RelayModeNone:
nodes = append(nodes, &node.Node{
Name: info.Name,
Tags: "",
Port: 0,
Address: info.ServerAddr,
ServerId: info.Id,
Protocol: info.Protocol,
Enabled: &enable,
})
default:
var relays []server.NodeRelay
err := json.Unmarshal([]byte(info.RelayNode), &relays)
if err != nil {
return nil, err
}
for _, relay := range relays {
nodes = append(nodes, &node.Node{
Name: relay.Prefix + info.Name,
Tags: "",
Port: uint16(relay.Port),
Address: relay.Host,
ServerId: info.Id,
Protocol: info.Protocol,
Enabled: &enable,
})
}
}
return nodes, nil
}

View File

@ -35,28 +35,29 @@ func (l *CreateSubscribeLogic) CreateSubscribe(req *types.CreateSubscribeRequest
discount = string(val)
}
sub := &subscribe.Subscribe{
Id: 0,
Name: req.Name,
Language: req.Language,
Description: req.Description,
UnitPrice: req.UnitPrice,
UnitTime: req.UnitTime,
Discount: discount,
Replacement: req.Replacement,
Inventory: req.Inventory,
Traffic: req.Traffic,
SpeedLimit: req.SpeedLimit,
DeviceLimit: req.DeviceLimit,
Quota: req.Quota,
Nodes: tool.Int64SliceToString(req.Nodes),
NodeTags: tool.StringSliceToString(req.NodeTags),
Show: req.Show,
Sell: req.Sell,
Sort: 0,
DeductionRatio: req.DeductionRatio,
AllowDeduction: req.AllowDeduction,
ResetCycle: req.ResetCycle,
RenewalReset: req.RenewalReset,
Id: 0,
Name: req.Name,
Language: req.Language,
Description: req.Description,
UnitPrice: req.UnitPrice,
UnitTime: req.UnitTime,
Discount: discount,
Replacement: req.Replacement,
Inventory: req.Inventory,
Traffic: req.Traffic,
SpeedLimit: req.SpeedLimit,
DeviceLimit: req.DeviceLimit,
Quota: req.Quota,
Nodes: tool.Int64SliceToString(req.Nodes),
NodeTags: tool.StringSliceToString(req.NodeTags),
Show: req.Show,
Sell: req.Sell,
Sort: 0,
DeductionRatio: req.DeductionRatio,
AllowDeduction: req.AllowDeduction,
ResetCycle: req.ResetCycle,
RenewalReset: req.RenewalReset,
ShowOriginalPrice: req.ShowOriginalPrice,
}
err := l.svcCtx.SubscribeModel.Insert(l.ctx, sub)
if err != nil {

View File

@ -43,28 +43,29 @@ func (l *UpdateSubscribeLogic) UpdateSubscribe(req *types.UpdateSubscribeRequest
discount = string(val)
}
sub := &subscribe.Subscribe{
Id: req.Id,
Name: req.Name,
Language: req.Language,
Description: req.Description,
UnitPrice: req.UnitPrice,
UnitTime: req.UnitTime,
Discount: discount,
Replacement: req.Replacement,
Inventory: req.Inventory,
Traffic: req.Traffic,
SpeedLimit: req.SpeedLimit,
DeviceLimit: req.DeviceLimit,
Quota: req.Quota,
Nodes: tool.Int64SliceToString(req.Nodes),
NodeTags: tool.StringSliceToString(req.NodeTags),
Show: req.Show,
Sell: req.Sell,
Sort: req.Sort,
DeductionRatio: req.DeductionRatio,
AllowDeduction: req.AllowDeduction,
ResetCycle: req.ResetCycle,
RenewalReset: req.RenewalReset,
Id: req.Id,
Name: req.Name,
Language: req.Language,
Description: req.Description,
UnitPrice: req.UnitPrice,
UnitTime: req.UnitTime,
Discount: discount,
Replacement: req.Replacement,
Inventory: req.Inventory,
Traffic: req.Traffic,
SpeedLimit: req.SpeedLimit,
DeviceLimit: req.DeviceLimit,
Quota: req.Quota,
Nodes: tool.Int64SliceToString(req.Nodes),
NodeTags: tool.StringSliceToString(req.NodeTags),
Show: req.Show,
Sell: req.Sell,
Sort: req.Sort,
DeductionRatio: req.DeductionRatio,
AllowDeduction: req.AllowDeduction,
ResetCycle: req.ResetCycle,
RenewalReset: req.RenewalReset,
ShowOriginalPrice: req.ShowOriginalPrice,
}
err = l.svcCtx.SubscribeModel.Update(l.ctx, sub)
if err != nil {

View File

@ -3,6 +3,7 @@ package system
import (
"context"
"encoding/json"
"github.com/perfect-panel/server/internal/config"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"

View File

@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"github.com/perfect-panel/server/initialize"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
@ -32,9 +33,12 @@ func (l *SetNodeMultiplierLogic) SetNodeMultiplier(req *types.SetNodeMultiplierR
l.Logger.Error("Marshal Node Multiplier Config Error: ", logger.Field("error", err.Error()))
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Marshal Node Multiplier Config Error: %s", err.Error())
}
if err := l.svcCtx.SystemModel.UpdateNodeMultiplierConfig(l.ctx, string(data)); err != nil {
if err = l.svcCtx.SystemModel.UpdateNodeMultiplierConfig(l.ctx, string(data)); err != nil {
l.Logger.Error("Update Node Multiplier Config Error: ", logger.Field("error", err.Error()))
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Update Node Multiplier Config Error: %s", err.Error())
}
// update Node Multiplier
initialize.Node(l.svcCtx)
return nil
}

View File

@ -30,6 +30,7 @@ func (l *GetUserListLogic) GetUserList(req *types.GetUserListRequest) (*types.Ge
list, total, err := l.svcCtx.UserModel.QueryPageList(l.ctx, req.Page, req.Size, &user.UserFilterParams{
UserId: req.UserId,
Search: req.Search,
Unscoped: req.Unscoped,
SubscribeId: req.SubscribeId,
UserSubscribeId: req.UserSubscribeId,
Order: "DESC",

View File

@ -41,6 +41,7 @@ func (l *GetUserSubscribeLogic) GetUserSubscribe(req *types.GetUserSubscribeList
for _, item := range data {
var sub types.UserSubscribe
tool.DeepCopy(&sub, item)
sub.Short, _ = tool.FixedUniqueString(item.Token, 8, "")
resp.List = append(resp.List, sub)
}
return

View File

@ -0,0 +1,55 @@
package user
import (
"context"
"fmt"
"time"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/uuidx"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
)
type ResetUserSubscribeTokenLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// NewResetUserSubscribeTokenLogic Reset user subscribe token
func NewResetUserSubscribeTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ResetUserSubscribeTokenLogic {
return &ResetUserSubscribeTokenLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *ResetUserSubscribeTokenLogic) ResetUserSubscribeToken(req *types.ResetUserSubscribeTokenRequest) error {
userSub, err := l.svcCtx.UserModel.FindOneSubscribe(l.ctx, req.UserSubscribeId)
if err != nil {
logger.Errorf("[ResetUserSubscribeToken] FindOneSubscribe error: %v", err.Error())
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindOneSubscribe error: %v", err.Error())
}
userSub.Token = uuidx.SubscribeToken(fmt.Sprintf("AdminUpdate:%d", time.Now().UnixMilli()))
err = l.svcCtx.UserModel.UpdateSubscribe(l.ctx, userSub)
if err != nil {
logger.Errorf("[ResetUserSubscribeToken] UpdateSubscribe error: %v", err.Error())
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "UpdateSubscribe error: %v", err.Error())
}
// Clear user subscribe cache
if err = l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, userSub); err != nil {
l.Errorw("ClearSubscribeCache failed:", logger.Field("error", err.Error()), logger.Field("userSubscribeId", userSub.Id))
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "ClearSubscribeCache failed: %v", err.Error())
}
// Clear subscribe cache
if err = l.svcCtx.SubscribeModel.ClearCache(l.ctx, userSub.SubscribeId); err != nil {
l.Errorw("failed to clear subscribe cache", logger.Field("error", err.Error()), logger.Field("subscribeId", userSub.SubscribeId))
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "failed to clear subscribe cache: %v", err.Error())
}
return nil
}

View File

@ -0,0 +1,53 @@
package user
import (
"context"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
)
type ResetUserSubscribeTrafficLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// NewResetUserSubscribeTrafficLogic Reset user subscribe traffic
func NewResetUserSubscribeTrafficLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ResetUserSubscribeTrafficLogic {
return &ResetUserSubscribeTrafficLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *ResetUserSubscribeTrafficLogic) ResetUserSubscribeTraffic(req *types.ResetUserSubscribeTrafficRequest) error {
userSub, err := l.svcCtx.UserModel.FindOneSubscribe(l.ctx, req.UserSubscribeId)
if err != nil {
l.Errorw("FindOneSubscribe error", logger.Field("error", err.Error()), logger.Field("userSubscribeId", req.UserSubscribeId))
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), " FindOneSubscribe error: %v", err.Error())
}
userSub.Download = 0
userSub.Upload = 0
err = l.svcCtx.UserModel.UpdateSubscribe(l.ctx, userSub)
if err != nil {
l.Errorw("UpdateSubscribe error", logger.Field("error", err.Error()), logger.Field("userSubscribeId", req.UserSubscribeId))
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), " UpdateSubscribe error: %v", err.Error())
}
// Clear user subscribe cache
if err = l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, userSub); err != nil {
l.Errorw("ClearSubscribeCache failed:", logger.Field("error", err.Error()), logger.Field("userSubscribeId", userSub.Id))
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "ClearSubscribeCache failed: %v", err.Error())
}
// Clear subscribe cache
if err = l.svcCtx.SubscribeModel.ClearCache(l.ctx, userSub.SubscribeId); err != nil {
l.Errorw("failed to clear subscribe cache", logger.Field("error", err.Error()), logger.Field("subscribeId", userSub.SubscribeId))
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "failed to clear subscribe cache: %v", err.Error())
}
return nil
}

View File

@ -0,0 +1,63 @@
package user
import (
"context"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
)
type ToggleUserSubscribeStatusLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// NewToggleUserSubscribeStatusLogic Stop user subscribe
func NewToggleUserSubscribeStatusLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ToggleUserSubscribeStatusLogic {
return &ToggleUserSubscribeStatusLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *ToggleUserSubscribeStatusLogic) ToggleUserSubscribeStatus(req *types.ToggleUserSubscribeStatusRequest) error {
userSub, err := l.svcCtx.UserModel.FindOneSubscribe(l.ctx, req.UserSubscribeId)
if err != nil {
l.Errorw("FindOneSubscribe error", logger.Field("error", err.Error()), logger.Field("userSubscribeId", req.UserSubscribeId))
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), " FindOneSubscribe error: %v", err.Error())
}
switch userSub.Status {
case 2: // active
userSub.Status = 5 // set status to stopped
case 5: // stopped
userSub.Status = 2 // set status to active
default:
l.Errorw("invalid user subscribe status", logger.Field("userSubscribeId", req.UserSubscribeId), logger.Field("status", userSub.Status))
return errors.Wrapf(xerr.NewErrCodeMsg(xerr.ERROR, "invalid subscribe status"), "invalid user subscribe status: %d", userSub.Status)
}
err = l.svcCtx.UserModel.UpdateSubscribe(l.ctx, userSub)
if err != nil {
l.Errorw("UpdateSubscribe error", logger.Field("error", err.Error()), logger.Field("userSubscribeId", req.UserSubscribeId))
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), " UpdateSubscribe error: %v", err.Error())
}
// Clear user subscribe cache
if err = l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, userSub); err != nil {
l.Errorw("ClearSubscribeCache failed:", logger.Field("error", err.Error()), logger.Field("userSubscribeId", userSub.Id))
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "ClearSubscribeCache failed: %v", err.Error())
}
// Clear subscribe cache
if err = l.svcCtx.SubscribeModel.ClearCache(l.ctx, userSub.SubscribeId); err != nil {
l.Errorw("failed to clear subscribe cache", logger.Field("error", err.Error()), logger.Field("subscribeId", userSub.SubscribeId))
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "failed to clear subscribe cache: %v", err.Error())
}
return nil
}

View File

@ -2,6 +2,7 @@ package auth
import (
"context"
"time"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
@ -206,6 +207,86 @@ func (l *BindDeviceLogic) rebindDeviceToNewUser(deviceInfo *user.Device, ip, use
//如果没有其他认证方式,禁用旧用户账号
if count < 1 {
//检查设备下是否有套餐,有套餐。就检查即将绑定过去的所有账户是否有套餐,如果有,那么检查两个套餐是否一致。如果一致就将即将删除的用户套餐,时间叠加到我绑定过去的用户套餐上面(如果套餐已过期就忽略)。新绑定设备的账户上套餐不一致或者不存在直接将套餐换绑即可
var oldUserSubscribes []user.Subscribe
err = tx.Where("user_id = ? AND status IN ?", oldUserId, []int64{0, 1}).Find(&oldUserSubscribes).Error
if err != nil {
l.Errorw("failed to query old user subscribes",
logger.Field("old_user_id", oldUserId),
logger.Field("error", err.Error()),
)
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query old user subscribes failed: %v", err)
}
if len(oldUserSubscribes) > 0 {
l.Infow("processing old user subscribes",
logger.Field("old_user_id", oldUserId),
logger.Field("new_user_id", newUserId),
logger.Field("subscribe_count", len(oldUserSubscribes)),
)
for _, oldSub := range oldUserSubscribes {
// 检查新用户是否有相同套餐ID的订阅
var newUserSub user.Subscribe
err = tx.Where("user_id = ? AND subscribe_id = ? AND status IN ?", newUserId, oldSub.SubscribeId, []int64{0, 1}).First(&newUserSub).Error
if err != nil {
// 新用户没有该套餐,直接换绑
oldSub.UserId = newUserId
if err := tx.Save(&oldSub).Error; err != nil {
l.Errorw("failed to rebind subscribe to new user",
logger.Field("subscribe_id", oldSub.Id),
logger.Field("old_user_id", oldUserId),
logger.Field("new_user_id", newUserId),
logger.Field("error", err.Error()),
)
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "rebind subscribe failed: %v", err)
}
l.Infow("rebind subscribe to new user",
logger.Field("subscribe_id", oldSub.Id),
logger.Field("new_user_id", newUserId),
)
} else {
// 新用户已有该套餐,检查旧套餐是否过期
now := time.Now()
if oldSub.ExpireTime.After(now) {
// 旧套餐未过期,叠加剩余时间
remainingDuration := oldSub.ExpireTime.Sub(now)
if newUserSub.ExpireTime.After(now) {
// 新套餐未过期,叠加时间
newUserSub.ExpireTime = newUserSub.ExpireTime.Add(remainingDuration)
} else {
newUserSub.ExpireTime = time.Now().Add(remainingDuration)
}
if err := tx.Save(&newUserSub).Error; err != nil {
l.Errorw("failed to update subscribe expire time",
logger.Field("subscribe_id", newUserSub.Id),
logger.Field("error", err.Error()),
)
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update subscribe expire time failed: %v", err)
}
l.Infow("merged subscribe time",
logger.Field("subscribe_id", newUserSub.Id),
logger.Field("new_expire_time", newUserSub.ExpireTime),
)
} else {
l.Infow("old subscribe expired, skip merge",
logger.Field("subscribe_id", oldSub.Id),
logger.Field("expire_time", oldSub.ExpireTime),
)
}
// 删除旧用户的套餐
if err := tx.Delete(&oldSub).Error; err != nil {
l.Errorw("failed to delete old subscribe",
logger.Field("subscribe_id", oldSub.Id),
logger.Field("error", err.Error()),
)
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete old subscribe failed: %v", err)
}
}
}
}
if err := tx.Model(&user.User{}).Where("id = ?", oldUserId).Delete(&user.User{}).Error; err != nil {
l.Errorw("failed to disable old user",
logger.Field("old_user_id", oldUserId),

View File

@ -68,6 +68,10 @@ func (l *UserLoginLogic) UserLogin(req *types.UserLoginRequest) (resp *types.Log
userInfo, err = l.svcCtx.UserModel.FindOneByEmail(l.ctx, req.Email)
if userInfo.DeletedAt.Valid {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "user email deleted: %v", req.Email)
}
if err != nil {
if errors.As(err, &gorm.ErrRecordNotFound) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "user email not exist: %v", req.Email)

View File

@ -79,12 +79,14 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp *
}
}
// Check if the user exists
_, err = l.svcCtx.UserModel.FindOneByEmail(l.ctx, req.Email)
u, err := l.svcCtx.UserModel.FindOneByEmail(l.ctx, req.Email)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
l.Errorw("FindOneByEmail Error", logger.Field("error", err))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user info failed: %v", err.Error())
} else if err == nil {
} else if err == nil && !u.DeletedAt.Valid {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserExist), "user email exist: %v", req.Email)
} else if err == nil && u.DeletedAt.Valid {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserDisabled), "user email deleted: %v", req.Email)
}
if !registerIpLimit(l.svcCtx, l.ctx, req.IP, "email", req.Email) {
@ -154,9 +156,8 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp *
time.Now().Unix(),
l.svcCtx.Config.JwtAuth.AccessExpire,
jwt.WithOption("UserId", userInfo.Id),
jwt.WithOption("identifier", req.Identifier),
jwt.WithOption("SessionId", sessionId),
jwt.WithOption("CtxLoginType", req.LoginType),
jwt.WithOption("LoginType", req.LoginType),
)
if err != nil {
l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error()))
@ -237,12 +238,5 @@ func (l *UserRegisterLogic) activeTrial(uid int64) error {
UUID: uuidx.NewUUID().String(),
Status: 1,
}
err = l.svcCtx.UserModel.InsertSubscribe(l.ctx, userSub)
if err != nil {
return err
}
if clearErr := l.svcCtx.NodeModel.ClearServerAllCache(l.ctx); clearErr != nil {
l.Errorf("ClearServerAllCache error: %v", clearErr.Error())
}
return err
return l.svcCtx.UserModel.InsertSubscribe(l.ctx, userSub)
}

View File

@ -51,6 +51,8 @@ func (l *GetGlobalConfigLogic) GetGlobalConfig() (resp *types.GetGlobalConfigRes
tool.SystemConfigSliceReflectToStruct(currencyCfg, &resp.Currency)
tool.SystemConfigSliceReflectToStruct(verifyCodeCfg, &resp.VerifyCode)
resp.Subscribe.SubscribePath = "/sub" + l.svcCtx.Config.Subscribe.SubscribePath
resp.Verify = types.VeifyConfig{
TurnstileSiteKey: l.svcCtx.Config.Verify.TurnstileSiteKey,
EnableLoginVerify: l.svcCtx.Config.Verify.LoginVerify,

View File

@ -11,7 +11,7 @@ import (
"time"
"github.com/perfect-panel/server/internal/config"
"github.com/perfect-panel/server/internal/model/server"
"github.com/perfect-panel/server/internal/model/node"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
@ -57,13 +57,13 @@ func (l *GetStatLogic) GetStat() (resp *types.GetStatResponse, err error) {
u = 1
}
var n int64
err = l.svcCtx.DB.Model(&server.Server{}).Where("enable = 1").Count(&n).Error
err = l.svcCtx.DB.Model(&node.Node{}).Where("enabled = 1").Count(&n).Error
if err != nil {
l.Logger.Error("[GetStatLogic] get server count failed: ", logger.Field("error", err.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get server count failed: %v", err.Error())
}
var nodeaddr []string
err = l.svcCtx.DB.Model(&server.Server{}).Where("enable = 1").Pluck("server_addr", &nodeaddr).Error
err = l.svcCtx.DB.Model(&node.Server{}).Pluck("address", &nodeaddr).Error
if err != nil {
l.Logger.Error("[GetStatLogic] get server_addr failed: ", logger.Field("error", err.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get server_addr failed: %v", err.Error())
@ -111,9 +111,23 @@ func (l *GetStatLogic) GetStat() (resp *types.GetStatResponse, err error) {
}
protocolDict := make(map[string]void)
var protocol []string
l.svcCtx.DB.Model(&server.Server{}).Where("enable = true").Pluck("protocol", &protocol)
err = l.svcCtx.DB.Model(&node.Node{}).Where("enabled = true").Pluck("protocol", &protocol).Error
if err != nil {
l.Logger.Error("[GetStatLogic] get protocol failed: ", logger.Field("error", err.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get protocol failed: %v", err.Error())
}
for _, p := range protocol {
protocolDict[p] = v
var protocols []node.Protocol
err = json.Unmarshal([]byte(p), &protocols)
if err != nil {
continue
}
for _, proto := range protocols {
if _, exists := protocolDict[proto.Type]; !exists {
protocolDict[proto.Type] = v
}
}
}
protocol = nil
for p := range protocolDict {

View File

@ -51,6 +51,16 @@ func (l *CloseOrderLogic) CloseOrder(req *types.CloseOrderRequest) error {
)
return nil
}
sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, orderInfo.SubscribeId)
if err != nil {
l.Errorw("[CloseOrder] Find subscribe info failed",
logger.Field("error", err.Error()),
logger.Field("subscribeId", orderInfo.SubscribeId),
)
return nil
}
err = l.svcCtx.DB.Transaction(func(tx *gorm.DB) error {
// update order status
err := tx.Model(&order.Order{}).Where("order_no = ?", req.OrderNo).Update("status", 3).Error
@ -124,9 +134,21 @@ func (l *CloseOrderLogic) CloseOrder(req *types.CloseOrderRequest) error {
// update user cache
return l.svcCtx.UserModel.UpdateUserCache(l.ctx, userInfo)
}
if sub.Inventory != -1 {
sub.Inventory++
if e := l.svcCtx.SubscribeModel.Update(l.ctx, sub, tx); e != nil {
l.Errorw("[CloseOrder] Restore subscribe inventory failed",
logger.Field("error", e.Error()),
logger.Field("subscribeId", sub.Id),
)
return e
}
}
return nil
})
if err != nil {
logger.Errorf("[CloseOrder] Transaction failed: %v", err.Error())
return err
}
return nil

View File

@ -81,6 +81,12 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
if !*sub.Sell {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "subscribe not sell")
}
// check subscribe plan inventory
if sub.Inventory == 0 {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SubscribeOutOfStock), "subscribe out of stock")
}
// check subscribe plan limit
if sub.Quota > 0 {
var count int64
@ -221,10 +227,23 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
return e
}
}
if sub.Inventory != -1 {
// decrease subscribe plan stock
sub.Inventory -= 1
// update subscribe plan stock
if err = l.svcCtx.SubscribeModel.Update(l.ctx, sub, db); err != nil {
l.Errorw("[Purchase] Database update error", logger.Field("error", err.Error()), logger.Field("subscribe", sub))
return err
}
}
// insert order
return db.WithContext(l.ctx).Model(&order.Order{}).Create(&orderInfo).Error
})
if err != nil {
l.Errorw("[Purchase] Database insert error", logger.Field("error", err.Error()), logger.Field("orderInfo", orderInfo))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert order error: %v", err.Error())
}
// Deferred task

View File

@ -9,6 +9,7 @@ import (
"github.com/perfect-panel/server/internal/model/log"
"github.com/perfect-panel/server/internal/report"
"github.com/perfect-panel/server/pkg/constant"
"github.com/perfect-panel/server/pkg/exchangeRate"
paymentPlatform "github.com/perfect-panel/server/pkg/payment"
@ -21,12 +22,10 @@ import (
"github.com/perfect-panel/server/internal/model/payment"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/exchangeRate"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/payment/alipay"
"github.com/perfect-panel/server/pkg/payment/epay"
"github.com/perfect-panel/server/pkg/payment/stripe"
"github.com/perfect-panel/server/pkg/tool"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
)
@ -261,6 +260,7 @@ func (l *PurchaseCheckoutLogic) stripePayment(config string, info *order.Order,
// epayPayment processes EPay payment by generating a payment URL for redirect
// It handles currency conversion and creates a payment URL for external payment processing
func (l *PurchaseCheckoutLogic) epayPayment(config *payment.Payment, info *order.Order, returnUrl string) (string, error) {
var err error
// Parse EPay configuration from payment settings
epayConfig := &payment.EPayConfig{}
if err := epayConfig.Unmarshal([]byte(config.Config)); err != nil {
@ -269,15 +269,18 @@ func (l *PurchaseCheckoutLogic) epayPayment(config *payment.Payment, info *order
}
// Initialize EPay client with merchant credentials
client := epay.NewClient(epayConfig.Pid, epayConfig.Url, epayConfig.Key, epayConfig.Type)
// Convert order amount to CNY using current exchange rate
amount, err := l.queryExchangeRate("CNY", info.Amount)
if err != nil {
return "", err
var amount float64
if l.svcCtx.Config.Currency.Unit != "CNY" {
// Convert order amount to CNY using current exchange rate
amount, err = l.queryExchangeRate("CNY", info.Amount)
if err != nil {
return "", err
}
} else {
amount = float64(info.Amount) / float64(100)
}
// gateway mod
isGatewayMod := report.IsGatewayMode()
// Build notification URL for payment status callbacks
@ -293,7 +296,6 @@ func (l *PurchaseCheckoutLogic) epayPayment(config *payment.Payment, info *order
if !ok {
host = l.svcCtx.Config.Host
}
notifyUrl = "https://" + host
if isGatewayMod {
notifyUrl += "/api"
@ -316,6 +318,7 @@ func (l *PurchaseCheckoutLogic) epayPayment(config *payment.Payment, info *order
// CryptoSaaSPayment processes CryptoSaaSPayment payment by generating a payment URL for redirect
// It handles currency conversion and creates a payment URL for external payment processing
func (l *PurchaseCheckoutLogic) CryptoSaaSPayment(config *payment.Payment, info *order.Order, returnUrl string) (string, error) {
var err error
// Parse EPay configuration from payment settings
epayConfig := &payment.CryptoSaaSConfig{}
if err := epayConfig.Unmarshal([]byte(config.Config)); err != nil {
@ -325,10 +328,16 @@ func (l *PurchaseCheckoutLogic) CryptoSaaSPayment(config *payment.Payment, info
// Initialize EPay client with merchant credentials
client := epay.NewClient(epayConfig.AccountID, epayConfig.Endpoint, epayConfig.SecretKey, epayConfig.Type)
// Convert order amount to CNY using current exchange rate
amount, err := l.queryExchangeRate("CNY", info.Amount)
if err != nil {
return "", err
var amount float64
if l.svcCtx.Config.Currency.Unit != "CNY" {
// Convert order amount to CNY using current exchange rate
amount, err = l.queryExchangeRate("CNY", info.Amount)
if err != nil {
return "", err
}
} else {
amount = float64(info.Amount) / float64(100)
}
// gateway mod
@ -354,6 +363,7 @@ func (l *PurchaseCheckoutLogic) CryptoSaaSPayment(config *payment.Payment, info
}
notifyUrl = notifyUrl + "/v1/notify/" + config.Platform + "/" + config.Token
}
// Create payment URL for user redirection
url := client.CreatePayUrl(epay.Order{
Name: l.svcCtx.Config.Site.SiteName,
@ -377,35 +387,18 @@ func (l *PurchaseCheckoutLogic) queryExchangeRate(to string, src int64) (amount
return amount, nil
}
// Retrieve system currency configuration
currency, err := l.svcCtx.SystemModel.GetCurrencyConfig(l.ctx)
if err != nil {
l.Errorw("[PurchaseCheckout] GetCurrencyConfig error", logger.Field("error", err.Error()))
return 0, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetCurrencyConfig error: %s", err.Error())
}
// Parse currency configuration
configs := struct {
CurrencyUnit string
CurrencySymbol string
AccessKey string
}{}
tool.SystemConfigSliceReflectToStruct(currency, &configs)
// Skip conversion if no exchange rate API key configured
if configs.AccessKey == "" {
if l.svcCtx.Config.Currency.AccessKey == "" {
return amount, nil
}
// Convert currency if system currency differs from target currency
if configs.CurrencyUnit != to {
result, err := exchangeRate.GetExchangeRete(configs.CurrencyUnit, to, configs.AccessKey, 1)
if err != nil {
return 0, err
}
amount = result * amount
result, err := exchangeRate.GetExchangeRete(l.svcCtx.Config.Currency.Unit, to, l.svcCtx.Config.Currency.AccessKey, 1)
if err != nil {
return 0, err
}
return amount, nil
l.svcCtx.ExchangeRate = result
return result * amount, nil
}
// balancePayment processes balance payment with gift amount priority logic

View File

@ -55,6 +55,12 @@ func (l *PurchaseLogic) Purchase(req *types.PortalPurchaseRequest) (resp *types.
l.Errorw("[Purchase] Database query error", logger.Field("error", err.Error()), logger.Field("subscribe_id", req.SubscribeId))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe error: %v", err.Error())
}
// check subscribe plan stock
if sub.Inventory == 0 {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SubscribeOutOfStock), "subscribe out of stock")
}
// check subscribe plan status
if !*sub.Sell {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "subscribe not sell")
@ -149,6 +155,16 @@ func (l *PurchaseLogic) Purchase(req *types.PortalPurchaseRequest) (resp *types.
return err
}
l.Infow("[Purchase] Guest order", logger.Field("order_no", orderInfo.OrderNo), logger.Field("identifier", req.Identifier))
// Decrease subscribe plan stock
if sub.Inventory != -1 {
sub.Inventory--
if e := l.svcCtx.SubscribeModel.Update(l.ctx, sub, tx); e != nil {
l.Errorw("[Purchase] Database update error", logger.Field("error", e.Error()), logger.Field("subscribe_id", sub.Id))
return e
}
}
// save guest order
if err = l.svcCtx.OrderModel.Insert(l.ctx, orderInfo, tx); err != nil {
return err

View File

@ -51,7 +51,7 @@ func (l *ServerPushUserTrafficLogic) ServerPushUserTraffic(req *types.ServerPush
if err != nil {
l.Errorw("[ServerPushUserTraffic] Push traffic task error", logger.Field("error", err.Error()), logger.Field("task", t))
} else {
l.Infow("[ServerPushUserTraffic] Push traffic task success", logger.Field("task", t), logger.Field("info", info))
l.Infow("[ServerPushUserTraffic] Push traffic task success", logger.Field("task", t.Type()), logger.Field("info", string(info.Payload)))
}
// Update server last reported time

View File

@ -10,6 +10,7 @@ import (
"github.com/perfect-panel/server/internal/model/client"
"github.com/perfect-panel/server/internal/model/log"
"github.com/perfect-panel/server/internal/model/node"
"github.com/perfect-panel/server/internal/report"
"github.com/perfect-panel/server/internal/model/user"
@ -106,10 +107,13 @@ func (l *SubscribeLogic) Handler(req *types.SubscribeRequest) (resp *types.Subsc
Download: userSubscribe.Download,
Upload: userSubscribe.Upload,
Traffic: userSubscribe.Traffic,
SubscribeURL: l.getSubscribeV2URL(req.Token),
SubscribeURL: l.getSubscribeV2URL(),
}),
adapter.WithParams(req.Params),
)
logger.Debugf("[SubscribeLogic] Building client config for user %d with URI %s", userSubscribe.UserId, l.getSubscribeV2URL())
// Get client config
adapterClient, err := a.Client()
if err != nil {
@ -143,17 +147,20 @@ func (l *SubscribeLogic) Handler(req *types.SubscribeRequest) (resp *types.Subsc
return
}
func (l *SubscribeLogic) getSubscribeV2URL(token string) string {
if l.svc.Config.Subscribe.PanDomain {
return fmt.Sprintf("https://%s", l.ctx.Request.Host)
}
func (l *SubscribeLogic) getSubscribeV2URL() string {
uri := l.ctx.Request.RequestURI
// is gateway mode, add /sub prefix
if report.IsGatewayMode() {
uri = "/sub" + uri
}
// use custom domain if configured
if l.svc.Config.Subscribe.SubscribeDomain != "" {
domains := strings.Split(l.svc.Config.Subscribe.SubscribeDomain, "\n")
return fmt.Sprintf("https://%s%s?token=%s", domains[0], l.svc.Config.Subscribe.SubscribePath, token)
return fmt.Sprintf("https://%s%s", domains[0], uri)
}
return fmt.Sprintf("https://%s%s?token=%s&", l.ctx.Request.Host, l.svc.Config.Subscribe.SubscribePath, token)
// use current request host
return fmt.Sprintf("https://%s%s", l.ctx.Request.Host, uri)
}
func (l *SubscribeLogic) getUserSubscribe(token string) (*user.Subscribe, error) {
@ -209,13 +216,16 @@ func (l *SubscribeLogic) getServers(userSub *user.Subscribe) ([]*node.Node, erro
}
nodeIds := tool.StringToInt64Slice(subDetails.Nodes)
tags := strings.Split(subDetails.NodeTags, ",")
l.Debugf("[Generate Subscribe]nodes: %v, NodeTags: %v", nodeIds, tags)
tags := tool.RemoveStringElement(strings.Split(subDetails.NodeTags, ","), "")
l.Debugf("[Generate Subscribe]nodes: %v, NodeTags: %v", len(nodeIds), len(tags))
if len(nodeIds) == 0 && len(tags) == 0 {
logger.Infow("[Generate Subscribe]no subscribe nodes")
return []*node.Node{}, nil
}
enable := true
_, nodes, err := l.svc.NodeModel.FilterNodeList(l.ctx.Request.Context(), &node.FilterNodeParams{
var nodes []*node.Node
_, nodes, err = l.svc.NodeModel.FilterNodeList(l.ctx.Request.Context(), &node.FilterNodeParams{
Page: 1,
Size: 1000,
NodeId: nodeIds,

View File

@ -27,6 +27,9 @@ type Filter struct {
// GetAnnouncementListByPage get announcement list by page
func (m *customAnnouncementModel) GetAnnouncementListByPage(ctx context.Context, page, size int, filter Filter) (int64, []*Announcement, error) {
if size == 0 {
size = 10
}
var list []*Announcement
var total int64
err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error {

View File

@ -1,141 +0,0 @@
package server
import (
"context"
"errors"
"fmt"
"github.com/perfect-panel/server/pkg/cache"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
var _ Model = (*customServerModel)(nil)
var (
cacheServerIdPrefix = "cache:server:id:"
)
type (
Model interface {
serverModel
customServerLogicModel
}
serverModel interface {
Insert(ctx context.Context, data *Server, tx ...*gorm.DB) error
FindOne(ctx context.Context, id int64) (*Server, error)
Update(ctx context.Context, data *Server, tx ...*gorm.DB) error
Delete(ctx context.Context, id int64, tx ...*gorm.DB) error
Transaction(ctx context.Context, fn func(db *gorm.DB) error) error
}
customServerModel struct {
*defaultServerModel
}
defaultServerModel struct {
cache.CachedConn
table string
}
)
func newServerModel(db *gorm.DB, c *redis.Client) *defaultServerModel {
return &defaultServerModel{
CachedConn: cache.NewConn(db, c),
table: "`Server`",
}
}
// NewModel returns a model for the database table.
func NewModel(conn *gorm.DB, c *redis.Client) Model {
return &customServerModel{
defaultServerModel: newServerModel(conn, c),
}
}
//nolint:unused
func (m *defaultServerModel) batchGetCacheKeys(Servers ...*Server) []string {
var keys []string
for _, server := range Servers {
keys = append(keys, m.getCacheKeys(server)...)
}
return keys
}
func (m *defaultServerModel) getCacheKeys(data *Server) []string {
if data == nil {
return []string{}
}
detailsKey := fmt.Sprintf("%s%v", CacheServerDetailPrefix, data.Id)
ServerIdKey := fmt.Sprintf("%s%v", cacheServerIdPrefix, data.Id)
//configIdKey := fmt.Sprintf("%s%v", config.ServerConfigCacheKey, data.Id)
//userIDKey := fmt.Sprintf("%s%d", config.ServerUserListCacheKey, data.Id)
// query protocols to get config keys
cacheKeys := []string{
ServerIdKey,
detailsKey,
//configIdKey,
//userIDKey,
}
return cacheKeys
}
func (m *defaultServerModel) Insert(ctx context.Context, data *Server, tx ...*gorm.DB) error {
err := m.ExecCtx(ctx, func(conn *gorm.DB) error {
if len(tx) > 0 {
conn = tx[0]
}
return conn.Create(&data).Error
}, m.getCacheKeys(data)...)
return err
}
func (m *defaultServerModel) FindOne(ctx context.Context, id int64) (*Server, error) {
ServerIdKey := fmt.Sprintf("%s%v", cacheServerIdPrefix, id)
var resp Server
err := m.QueryCtx(ctx, &resp, ServerIdKey, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&Server{}).Where("`id` = ?", id).First(&resp).Error
})
switch {
case err == nil:
return &resp, nil
default:
return nil, err
}
}
func (m *defaultServerModel) Update(ctx context.Context, data *Server, tx ...*gorm.DB) error {
old, err := m.FindOne(ctx, data.Id)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
err = m.ExecCtx(ctx, func(conn *gorm.DB) error {
if len(tx) > 0 {
conn = tx[0]
}
return conn.Save(data).Error
}, m.getCacheKeys(old)...)
return err
}
func (m *defaultServerModel) Delete(ctx context.Context, id int64, tx ...*gorm.DB) error {
data, err := m.FindOne(ctx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
return err
}
err = m.ExecCtx(ctx, func(conn *gorm.DB) error {
if len(tx) > 0 {
conn = tx[0]
}
return conn.Delete(&Server{}, id).Error
}, m.getCacheKeys(data)...)
return err
}
func (m *defaultServerModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error {
return m.TransactCtx(ctx, fn)
}

View File

@ -1,292 +0,0 @@
package server
import (
"context"
"fmt"
"strings"
"gorm.io/gorm"
)
type customServerLogicModel interface {
FindServerListByFilter(ctx context.Context, filter *ServerFilter) (total int64, list []*Server, err error)
ClearCache(ctx context.Context, id int64) error
QueryServerCountByServerGroups(ctx context.Context, groupIds []int64) (int64, error)
QueryAllGroup(ctx context.Context) ([]*Group, error)
BatchDeleteNodeGroup(ctx context.Context, ids []int64) error
InsertGroup(ctx context.Context, data *Group) error
FindOneGroup(ctx context.Context, id int64) (*Group, error)
UpdateGroup(ctx context.Context, data *Group) error
DeleteGroup(ctx context.Context, id int64) error
FindServerDetailByGroupIdsAndIds(ctx context.Context, groupId, ids []int64) ([]*Server, error)
FindServerListByGroupIds(ctx context.Context, groupId []int64) ([]*Server, error)
FindAllServer(ctx context.Context) ([]*Server, error)
FindNodeByServerAddrAndProtocol(ctx context.Context, serverAddr string, protocol string) ([]*Server, error)
FindServerMinSortByIds(ctx context.Context, ids []int64) (int64, error)
FindServerListByIds(ctx context.Context, ids []int64) ([]*Server, error)
InsertRuleGroup(ctx context.Context, data *RuleGroup) error
FindOneRuleGroup(ctx context.Context, id int64) (*RuleGroup, error)
UpdateRuleGroup(ctx context.Context, data *RuleGroup) error
DeleteRuleGroup(ctx context.Context, id int64) error
QueryAllRuleGroup(ctx context.Context) ([]*RuleGroup, error)
FindServersByTag(ctx context.Context, tag string) ([]*Server, error)
FindServerTags(ctx context.Context) ([]string, error)
SetDefaultRuleGroup(ctx context.Context, id int64) error
}
var (
CacheServerDetailPrefix = "cache:server:detail:"
cacheServerGroupAllKeys = "cache:serverGroup:all"
cacheServerRuleGroupAllKeys = "cache:serverRuleGroup:all"
)
// ClearCache Clear Cache
func (m *customServerModel) ClearCache(ctx context.Context, id int64) error {
serverIdKey := fmt.Sprintf("%s%v", cacheServerIdPrefix, id)
//configKey := fmt.Sprintf("%s%d", config.ServerConfigCacheKey, id)
//userListKey := fmt.Sprintf("%s%v", config.ServerUserListCacheKey, id)
return m.DelCacheCtx(ctx, serverIdKey)
}
// QueryServerCountByServerGroups Query Server Count By Server Groups
func (m *customServerModel) QueryServerCountByServerGroups(ctx context.Context, groupIds []int64) (int64, error) {
var count int64
err := m.QueryNoCacheCtx(ctx, &count, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&Server{}).Where("group_id IN ?", groupIds).Count(&count).Error
})
return count, err
}
// QueryAllGroup returns all groups.
func (m *customServerModel) QueryAllGroup(ctx context.Context) ([]*Group, error) {
var groups []*Group
err := m.QueryCtx(ctx, &groups, cacheServerGroupAllKeys, func(conn *gorm.DB, v interface{}) error {
return conn.Find(&groups).Error
})
return groups, err
}
// BatchDeleteNodeGroup deletes multiple groups.
func (m *customServerModel) BatchDeleteNodeGroup(ctx context.Context, ids []int64) error {
return m.Transaction(ctx, func(tx *gorm.DB) error {
for _, id := range ids {
if err := m.Delete(ctx, id); err != nil {
return err
}
}
return nil
})
}
// InsertGroup inserts a group.
func (m *customServerModel) InsertGroup(ctx context.Context, data *Group) error {
return m.ExecCtx(ctx, func(conn *gorm.DB) error {
return conn.Create(data).Error
}, cacheServerGroupAllKeys)
}
// FindOneGroup finds a group.
func (m *customServerModel) FindOneGroup(ctx context.Context, id int64) (*Group, error) {
var group Group
err := m.QueryCtx(ctx, &group, fmt.Sprintf("cache:serverGroup:%v", id), func(conn *gorm.DB, v interface{}) error {
return conn.Model(&Group{}).Where("id = ?", id).First(&group).Error
})
return &group, err
}
// UpdateGroup updates a group.
func (m *customServerModel) UpdateGroup(ctx context.Context, data *Group) error {
return m.ExecCtx(ctx, func(conn *gorm.DB) error {
return conn.Model(&Group{}).Where("id = ?", data.Id).Updates(data).Error
}, cacheServerGroupAllKeys, fmt.Sprintf("cache:serverGroup:%v", data.Id))
}
// DeleteGroup deletes a group.
func (m *customServerModel) DeleteGroup(ctx context.Context, id int64) error {
return m.ExecCtx(ctx, func(conn *gorm.DB) error {
return conn.Where("id = ?", id).Delete(&Group{}).Error
}, cacheServerGroupAllKeys, fmt.Sprintf("cache:serverGroup:%v", id))
}
// FindServerDetailByGroupIdsAndIds finds server details by group IDs and IDs.
func (m *customServerModel) FindServerDetailByGroupIdsAndIds(ctx context.Context, groupId, ids []int64) ([]*Server, error) {
if len(groupId) == 0 && len(ids) == 0 {
return []*Server{}, nil
}
var list []*Server
err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error {
conn = conn.
Model(&Server{}).
Where("`enable` = ?", true)
if len(groupId) > 0 && len(ids) > 0 {
// OR is used to connect group_id and id conditions
conn = conn.Where("(`group_id` IN ? OR `id` IN ?)", groupId, ids)
} else if len(groupId) > 0 {
conn = conn.Where("`group_id` IN ?", groupId)
} else if len(ids) > 0 {
conn = conn.Where("`id` IN ?", ids)
}
return conn.Order("sort ASC").Find(v).Error
})
return list, err
}
func (m *customServerModel) FindServerListByGroupIds(ctx context.Context, groupId []int64) ([]*Server, error) {
var data []*Server
err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&Server{}).Where("group_id IN ?", groupId).Find(v).Error
})
return data, err
}
func (m *customServerModel) FindAllServer(ctx context.Context) ([]*Server, error) {
var data []*Server
err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&Server{}).Order("sort ASC").Find(v).Error
})
return data, err
}
func (m *customServerModel) FindNodeByServerAddrAndProtocol(ctx context.Context, serverAddr string, protocol string) ([]*Server, error) {
var data []*Server
err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&Server{}).Where("server_addr = ? and protocol = ?", serverAddr, protocol).Order("sort ASC").Find(v).Error
})
return data, err
}
func (m *customServerModel) FindServerMinSortByIds(ctx context.Context, ids []int64) (int64, error) {
var minSort int64
err := m.QueryNoCacheCtx(ctx, &minSort, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&Server{}).Where("id IN ?", ids).Select("COALESCE(MIN(sort), 0)").Scan(v).Error
})
return minSort, err
}
func (m *customServerModel) FindServerListByIds(ctx context.Context, ids []int64) ([]*Server, error) {
var list []*Server
err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&Server{}).Where("id IN ?", ids).Find(v).Error
})
return list, err
}
// InsertRuleGroup inserts a group.
func (m *customServerModel) InsertRuleGroup(ctx context.Context, data *RuleGroup) error {
return m.ExecCtx(ctx, func(conn *gorm.DB) error {
return conn.Where(&RuleGroup{}).Create(data).Error
}, cacheServerRuleGroupAllKeys, fmt.Sprintf("cache:serverRuleGroup:%v", data.Id))
}
// FindOneRuleGroup finds a group.
func (m *customServerModel) FindOneRuleGroup(ctx context.Context, id int64) (*RuleGroup, error) {
var group RuleGroup
err := m.QueryCtx(ctx, &group, fmt.Sprintf("cache:serverRuleGroup:%v", id), func(conn *gorm.DB, v interface{}) error {
return conn.Where(&RuleGroup{}).Model(&RuleGroup{}).Where("id = ?", id).First(&group).Error
})
return &group, err
}
// UpdateRuleGroup updates a group.
func (m *customServerModel) UpdateRuleGroup(ctx context.Context, data *RuleGroup) error {
return m.ExecCtx(ctx, func(conn *gorm.DB) error {
return conn.Where(&RuleGroup{}).Model(&RuleGroup{}).Where("id = ?", data.Id).Save(data).Error
}, cacheServerRuleGroupAllKeys, fmt.Sprintf("cache:serverRuleGroup:%v", data.Id))
}
// DeleteRuleGroup deletes a group.
func (m *customServerModel) DeleteRuleGroup(ctx context.Context, id int64) error {
return m.ExecCtx(ctx, func(conn *gorm.DB) error {
return conn.Where(&RuleGroup{}).Where("id = ?", id).Delete(&RuleGroup{}).Error
}, cacheServerRuleGroupAllKeys, fmt.Sprintf("cache:serverRuleGroup:%v", id))
}
// QueryAllRuleGroup returns all rule groups.
func (m *customServerModel) QueryAllRuleGroup(ctx context.Context) ([]*RuleGroup, error) {
var groups []*RuleGroup
err := m.QueryCtx(ctx, &groups, cacheServerRuleGroupAllKeys, func(conn *gorm.DB, v interface{}) error {
return conn.Where(&RuleGroup{}).Find(&groups).Error
})
return groups, err
}
func (m *customServerModel) FindServerListByFilter(ctx context.Context, filter *ServerFilter) (total int64, list []*Server, err error) {
var data []*Server
if filter == nil {
filter = &ServerFilter{
Page: 1,
Size: 10,
}
}
if filter.Page <= 0 {
filter.Page = 1
}
if filter.Size <= 0 {
filter.Size = 10
}
err = m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error {
query := conn.Model(&Server{}).Order("sort ASC")
if filter.Group > 0 {
query = conn.Where("group_id = ?", filter.Group)
}
if filter.Search != "" {
query = query.Where("name LIKE ? OR server_addr LIKE ? OR tags LIKE ?", "%"+filter.Search+"%", "%"+filter.Search+"%", "%"+filter.Search+"%")
}
if len(filter.Tags) > 0 {
for i, tag := range filter.Tags {
if i == 0 {
query = query.Where("tags LIKE ?", "%"+tag+"%")
} else {
query = query.Or("tags LIKE ?", "%"+tag+"%")
}
}
}
return query.Count(&total).Limit(filter.Size).Offset((filter.Page - 1) * filter.Size).Find(v).Error
})
if err != nil {
return 0, nil, err
}
return total, data, nil
}
func (m *customServerModel) FindServerTags(ctx context.Context) ([]string, error) {
var data []string
err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&Server{}).Distinct("tags").Pluck("tags", v).Error
})
var tags []string
for _, tag := range data {
if strings.Contains(tag, ",") {
tags = append(tags, strings.Split(tag, ",")...)
} else {
tags = append(tags, tag)
}
}
return tags, err
}
func (m *customServerModel) FindServersByTag(ctx context.Context, tag string) ([]*Server, error) {
var data []*Server
err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&Server{}).Where("FIND_IN_SET(?, tags)", tag).Order("sort ASC").Find(v).Error
})
return data, err
}
// SetDefaultRuleGroup sets the default rule group.
func (m *customServerModel) SetDefaultRuleGroup(ctx context.Context, id int64) error {
return m.ExecCtx(ctx, func(conn *gorm.DB) error {
// Reset all groups to not default
if err := conn.Model(&RuleGroup{}).Where("`id` != ?", id).Update("default", false).Error; err != nil {
return err
}
// Set the specified group as default
return conn.Model(&RuleGroup{}).Where("`id` = ?", id).Update("default", true).Error
}, cacheServerRuleGroupAllKeys, fmt.Sprintf("cache:serverRuleGroup:%v", id))
}

View File

@ -1,219 +0,0 @@
package server
import (
"time"
"github.com/perfect-panel/server/pkg/logger"
"gorm.io/gorm"
)
const (
RelayModeNone = "none"
RelayModeAll = "all"
RelayModeRandom = "random"
RuleGroupTypeReject = "reject"
RuleGroupTypeDefault = "default"
RuleGroupTypeDirect = "direct"
)
type ServerFilter struct {
Id int64
Tags []string
Group int64
Search string
Page int
Size int
}
// Deprecated: use internal/model/node/server.go
type Server struct {
Id int64 `gorm:"primary_key"`
Name string `gorm:"type:varchar(100);not null;default:'';comment:Node Name"`
Tags string `gorm:"type:varchar(128);not null;default:'';comment:Tags"`
Country string `gorm:"type:varchar(128);not null;default:'';comment:Country"`
City string `gorm:"type:varchar(128);not null;default:'';comment:City"`
Latitude string `gorm:"type:varchar(128);not null;default:'';comment:Latitude"`
Longitude string `gorm:"type:varchar(128);not null;default:'';comment:Longitude"`
ServerAddr string `gorm:"type:varchar(100);not null;default:'';comment:Server Address"`
RelayMode string `gorm:"type:varchar(20);not null;default:'none';comment:Relay Mode"`
RelayNode string `gorm:"type:text;comment:Relay Node"`
SpeedLimit int `gorm:"type:int;not null;default:0;comment:Speed Limit"`
TrafficRatio float32 `gorm:"type:DECIMAL(4,2);not null;default:0;comment:Traffic Ratio"`
GroupId int64 `gorm:"index:idx_group_id;type:int;default:null;comment:Group ID"`
Protocol string `gorm:"type:varchar(20);not null;default:'';comment:Protocol"`
Config string `gorm:"type:text;comment:Config"`
Enable *bool `gorm:"type:tinyint(1);not null;default:1;comment:Enabled"`
Sort int64 `gorm:"type:int;not null;default:0;comment:Sort"`
LastReportedAt time.Time `gorm:"comment:Last Reported Time"`
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
UpdatedAt time.Time `gorm:"comment:Update Time"`
}
func (*Server) TableName() string {
return "server"
}
func (s *Server) BeforeDelete(tx *gorm.DB) error {
logger.Debugf("[Server] BeforeDelete")
if err := tx.Exec("UPDATE `server` SET sort = sort - 1 WHERE sort > ?", s.Sort).Error; err != nil {
return err
}
return nil
}
func (s *Server) BeforeUpdate(tx *gorm.DB) error {
logger.Debugf("[Server] BeforeUpdate")
var count int64
if err := tx.Set("gorm:query_option", "FOR UPDATE").Model(&Server{}).
Where("sort = ? AND id != ?", s.Sort, s.Id).Count(&count).Error; err != nil {
return err
}
if count > 1 {
// reorder sort
if err := reorderSort(tx); err != nil {
logger.Errorf("[Server] BeforeUpdate reorderSort error: %v", err.Error())
return err
}
// get max sort
var maxSort int64
if err := tx.Model(&Server{}).Select("MAX(sort)").Scan(&maxSort).Error; err != nil {
return err
}
s.Sort = maxSort + 1
}
return nil
}
func (s *Server) BeforeCreate(tx *gorm.DB) error {
logger.Debugf("[Server] BeforeCreate")
if s.Sort == 0 {
var maxSort int64
if err := tx.Model(&Server{}).Select("COALESCE(MAX(sort), 0)").Scan(&maxSort).Error; err != nil {
return err
}
s.Sort = maxSort + 1
}
return nil
}
type Vless struct {
Port int `json:"port"`
Flow string `json:"flow"`
Transport string `json:"transport"`
TransportConfig TransportConfig `json:"transport_config"`
Security string `json:"security"`
SecurityConfig SecurityConfig `json:"security_config"`
}
type Vmess struct {
Port int `json:"port"`
Flow string `json:"flow"`
Transport string `json:"transport"`
TransportConfig TransportConfig `json:"transport_config"`
Security string `json:"security"`
SecurityConfig SecurityConfig `json:"security_config"`
}
type Trojan struct {
Port int `json:"port"`
Flow string `json:"flow"`
Transport string `json:"transport"`
TransportConfig TransportConfig `json:"transport_config"`
Security string `json:"security"`
SecurityConfig SecurityConfig `json:"security_config"`
}
type Shadowsocks struct {
Method string `json:"method"`
Port int `json:"port"`
ServerKey string `json:"server_key"`
}
type Hysteria2 struct {
Port int `json:"port"`
HopPorts string `json:"hop_ports"`
HopInterval int `json:"hop_interval"`
ObfsPassword string `json:"obfs_password"`
SecurityConfig SecurityConfig `json:"security_config"`
}
type Tuic struct {
Port int `json:"port"`
DisableSNI bool `json:"disable_sni"`
ReduceRtt bool `json:"reduce_rtt"`
UDPRelayMode string `json:"udp_relay_mode"`
CongestionController string `json:"congestion_controller"`
SecurityConfig SecurityConfig `json:"security_config"`
}
type AnyTLS struct {
Port int `json:"port"`
SecurityConfig SecurityConfig `json:"security_config"`
}
type TransportConfig struct {
Path string `json:"path,omitempty"` // ws/httpupgrade
Host string `json:"host,omitempty"`
ServiceName string `json:"service_name"` // grpc
}
type SecurityConfig struct {
SNI string `json:"sni"`
AllowInsecure bool `json:"allow_insecure"`
Fingerprint string `json:"fingerprint"`
RealityServerAddr string `json:"reality_server_addr"`
RealityServerPort int `json:"reality_server_port"`
RealityPrivateKey string `json:"reality_private_key"`
RealityPublicKey string `json:"reality_public_key"`
RealityShortId string `json:"reality_short_id"`
}
type NodeRelay struct {
Host string `json:"host"`
Port int `json:"port"`
Prefix string `json:"prefix"`
}
type Group struct {
Id int64 `gorm:"primary_key"`
Name string `gorm:"type:varchar(100);not null;default:'';comment:Group Name"`
Description string `gorm:"type:varchar(255);default:'';comment:Group Description"`
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
UpdatedAt time.Time `gorm:"comment:Update Time"`
}
func (Group) TableName() string {
return "server_group"
}
type RuleGroup struct {
Id int64 `gorm:"primary_key"`
Icon string `gorm:"type:MEDIUMTEXT;comment:Rule Group Icon"`
Name string `gorm:"type:varchar(100);not null;default:'';comment:Rule Group Name"`
Type string `gorm:"type:varchar(100);not null;default:'';comment:Rule Group Type"`
Tags string `gorm:"type:text;comment:Selected Node Tags"`
Rules string `gorm:"type:MEDIUMTEXT;comment:Rules"`
Enable bool `gorm:"type:tinyint(1);not null;default:1;comment:Rule Group Enable"`
Default bool `gorm:"type:tinyint(1);not null;default:0;comment:Rule Group is Default"`
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
UpdatedAt time.Time `gorm:"comment:Update Time"`
}
func (RuleGroup) TableName() string {
return "server_rule_group"
}
func reorderSort(tx *gorm.DB) error {
var servers []Server
if err := tx.Order("sort, id").Find(&servers).Error; err != nil {
return err
}
for i, server := range servers {
if server.Sort != int64(i)+1 {
if err := tx.Exec("UPDATE `server` SET sort = ? WHERE id = ?", i+1, server.Id).Error; err != nil {
return err
}
}
}
return nil
}

View File

@ -7,30 +7,31 @@ import (
)
type Subscribe struct {
Id int64 `gorm:"primaryKey"`
Name string `gorm:"type:varchar(255);not null;default:'';comment:Subscribe Name"`
Language string `gorm:"type:varchar(255);not null;default:'';comment:Language"`
Description string `gorm:"type:text;comment:Subscribe Description"`
UnitPrice int64 `gorm:"type:int;not null;default:0;comment:Unit Price"`
UnitTime string `gorm:"type:varchar(255);not null;default:'';comment:Unit Time"`
Discount string `gorm:"type:text;comment:Discount"`
Replacement int64 `gorm:"type:int;not null;default:0;comment:Replacement"`
Inventory int64 `gorm:"type:int;not null;default:0;comment:Inventory"`
Traffic int64 `gorm:"type:int;not null;default:0;comment:Traffic"`
SpeedLimit int64 `gorm:"type:int;not null;default:0;comment:Speed Limit"`
DeviceLimit int64 `gorm:"type:int;not null;default:0;comment:Device Limit"`
Quota int64 `gorm:"type:int;not null;default:0;comment:Quota"`
Nodes string `gorm:"type:varchar(255);comment:Node Ids"`
NodeTags string `gorm:"type:varchar(255);comment:Node Tags"`
Show *bool `gorm:"type:tinyint(1);not null;default:0;comment:Show portal page"`
Sell *bool `gorm:"type:tinyint(1);not null;default:0;comment:Sell"`
Sort int64 `gorm:"type:int;not null;default:0;comment:Sort"`
DeductionRatio int64 `gorm:"type:int;default:0;comment:Deduction Ratio"`
AllowDeduction *bool `gorm:"type:tinyint(1);default:1;comment:Allow deduction"`
ResetCycle int64 `gorm:"type:int;default:0;comment:Reset Cycle: 0: No Reset, 1: 1st, 2: Monthly, 3: Yearly"`
RenewalReset *bool `gorm:"type:tinyint(1);default:0;comment:Renew Reset"`
CreatedAt time.Time `gorm:"<-:create;comment:Create Time"`
UpdatedAt time.Time `gorm:"comment:Update Time"`
Id int64 `gorm:"primaryKey"`
Name string `gorm:"type:varchar(255);not null;default:'';comment:Subscribe Name"`
Language string `gorm:"type:varchar(255);not null;default:'';comment:Language"`
Description string `gorm:"type:text;comment:Subscribe Description"`
UnitPrice int64 `gorm:"type:int;not null;default:0;comment:Unit Price"`
UnitTime string `gorm:"type:varchar(255);not null;default:'';comment:Unit Time"`
Discount string `gorm:"type:text;comment:Discount"`
Replacement int64 `gorm:"type:int;not null;default:0;comment:Replacement"`
Inventory int64 `gorm:"type:int;not null;default:-1;comment:Inventory"`
Traffic int64 `gorm:"type:int;not null;default:0;comment:Traffic"`
SpeedLimit int64 `gorm:"type:int;not null;default:0;comment:Speed Limit"`
DeviceLimit int64 `gorm:"type:int;not null;default:0;comment:Device Limit"`
Quota int64 `gorm:"type:int;not null;default:0;comment:Quota"`
Nodes string `gorm:"type:varchar(255);comment:Node Ids"`
NodeTags string `gorm:"type:varchar(255);comment:Node Tags"`
Show *bool `gorm:"type:tinyint(1);not null;default:0;comment:Show portal page"`
Sell *bool `gorm:"type:tinyint(1);not null;default:0;comment:Sell"`
Sort int64 `gorm:"type:int;not null;default:0;comment:Sort"`
DeductionRatio int64 `gorm:"type:int;default:0;comment:Deduction Ratio"`
AllowDeduction *bool `gorm:"type:tinyint(1);default:1;comment:Allow deduction"`
ResetCycle int64 `gorm:"type:int;default:0;comment:Reset Cycle: 0: No Reset, 1: 1st, 2: Monthly, 3: Yearly"`
RenewalReset *bool `gorm:"type:tinyint(1);default:0;comment:Renew Reset"`
ShowOriginalPrice bool `gorm:"type:tinyint(1);not null;default:1;comment:Show Original Price"`
CreatedAt time.Time `gorm:"<-:create;comment:Create Time"`
UpdatedAt time.Time `gorm:"comment:Update Time"`
}
func (*Subscribe) TableName() string {

View File

@ -6,6 +6,7 @@ import (
"fmt"
"github.com/perfect-panel/server/pkg/cache"
"github.com/perfect-panel/server/pkg/logger"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
@ -72,7 +73,7 @@ func (m *defaultUserModel) FindOneByEmail(ctx context.Context, email string) (*U
if err := conn.Model(&AuthMethods{}).Where("`auth_type` = 'email' AND `auth_identifier` = ?", email).First(&data).Error; err != nil {
return err
}
return conn.Model(&User{}).Where("`id` = ?", data.UserId).Preload("UserDevices").Preload("AuthMethods").First(v).Error
return conn.Model(&User{}).Unscoped().Where("`id` = ?", data.UserId).Preload("UserDevices").Preload("AuthMethods").First(v).Error
})
return &user, err
}
@ -91,7 +92,7 @@ func (m *defaultUserModel) FindOne(ctx context.Context, id int64) (*User, error)
userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, id)
var resp User
err := m.QueryCtx(ctx, &resp, userIdKey, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&User{}).Where("`id` = ?", id).Preload("UserDevices").Preload("AuthMethods").First(&resp).Error
return conn.Model(&User{}).Unscoped().Where("`id` = ?", id).Preload("UserDevices").Preload("AuthMethods").First(&resp).Error
})
return &resp, err
}
@ -119,10 +120,11 @@ func (m *defaultUserModel) Delete(ctx context.Context, id int64, tx ...*gorm.DB)
return err
}
// 使用批量相关缓存清理,包含所有相关数据的缓存
// Use batch related cache cleaning, including a cache of all relevant data
defer func() {
if clearErr := m.BatchClearRelatedCache(ctx, data); clearErr != nil {
// 记录清理缓存错误,但不阻断删除操作
// Record cache cleaning errors, but do not block deletion operations
logger.Errorf("failed to clear related cache for user %d: %v", id, clearErr.Error())
}
}()
@ -130,24 +132,11 @@ func (m *defaultUserModel) Delete(ctx context.Context, id int64, tx ...*gorm.DB)
if len(tx) > 0 {
db = tx[0]
}
// 删除用户相关的所有数据
// Soft deletion of user information without any processing of other information (Determine whether to allow login/subscription based on the user's deletion status)
if err := db.Model(&User{}).Where("`id` = ?", id).Delete(&User{}).Error; err != nil {
return err
}
if err := db.Model(&AuthMethods{}).Where("`user_id` = ?", id).Delete(&AuthMethods{}).Error; err != nil {
return err
}
if err := db.Model(&Subscribe{}).Where("`user_id` = ?", id).Delete(&Subscribe{}).Error; err != nil {
return err
}
if err := db.Model(&Device{}).Where("`user_id` = ?", id).Delete(&Device{}).Error; err != nil {
return err
}
return nil
})
}

View File

@ -62,6 +62,7 @@ type UserFilterParams struct {
SubscribeId *int64
UserSubscribeId *int64
Order string // Order by id, e.g., "desc"
Unscoped bool // Whether to include soft-deleted records
}
type customUserLogicModel interface {
@ -148,6 +149,9 @@ func (m *customUserModel) QueryPageList(ctx context.Context, page, size int, fil
if filter.Order != "" {
conn = conn.Order(fmt.Sprintf("user.id %s", filter.Order))
}
if filter.Unscoped {
conn = conn.Unscoped()
}
}
return conn.Model(&User{}).Group("user.id").Count(&total).Limit(size).Offset((page-1)*size).Preload("UserDevices").Preload("AuthMethods", func(db *gorm.DB) *gorm.DB { return db.Order("user_auth_methods.auth_type desc") }).Find(&list).Error
})

View File

@ -2,32 +2,35 @@ package user
import (
"time"
"gorm.io/gorm"
)
type User struct {
Id int64 `gorm:"primaryKey"`
Password string `gorm:"type:varchar(100);not null;comment:User Password"`
Algo string `gorm:"type:varchar(20);default:'default';comment:Encryption Algorithm"`
Salt string `gorm:"type:varchar(20);default:null;comment:Password Salt"`
Avatar string `gorm:"type:MEDIUMTEXT;comment:User Avatar"`
Balance int64 `gorm:"default:0;comment:User Balance"` // User Balance Amount
ReferCode string `gorm:"type:varchar(20);default:'';comment:Referral Code"`
RefererId int64 `gorm:"index:idx_referer;comment:Referrer ID"`
Commission int64 `gorm:"default:0;comment:Commission"` // Commission Amount
ReferralPercentage uint8 `gorm:"default:0;comment:Referral"` // Referral Percentage
OnlyFirstPurchase *bool `gorm:"default:true;not null;comment:Only First Purchase"` // Only First Purchase Referral
GiftAmount int64 `gorm:"default:0;comment:User Gift Amount"`
Enable *bool `gorm:"default:true;not null;comment:Is Account Enabled"`
IsAdmin *bool `gorm:"default:false;not null;comment:Is Admin"`
EnableBalanceNotify *bool `gorm:"default:false;not null;comment:Enable Balance Change Notifications"`
EnableLoginNotify *bool `gorm:"default:false;not null;comment:Enable Login Notifications"`
EnableSubscribeNotify *bool `gorm:"default:false;not null;comment:Enable Subscription Notifications"`
EnableTradeNotify *bool `gorm:"default:false;not null;comment:Enable Trade Notifications"`
AuthMethods []AuthMethods `gorm:"foreignKey:UserId;references:Id"`
UserDevices []Device `gorm:"foreignKey:UserId;references:Id"`
Rules string `gorm:"type:TEXT;comment:User Rules"`
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
UpdatedAt time.Time `gorm:"comment:Update Time"`
Id int64 `gorm:"primaryKey"`
Password string `gorm:"type:varchar(100);not null;comment:User Password"`
Algo string `gorm:"type:varchar(20);default:'default';comment:Encryption Algorithm"`
Salt string `gorm:"type:varchar(20);default:null;comment:Password Salt"`
Avatar string `gorm:"type:MEDIUMTEXT;comment:User Avatar"`
Balance int64 `gorm:"default:0;comment:User Balance"` // User Balance Amount
ReferCode string `gorm:"type:varchar(20);default:'';comment:Referral Code"`
RefererId int64 `gorm:"index:idx_referer;comment:Referrer ID"`
Commission int64 `gorm:"default:0;comment:Commission"` // Commission Amount
ReferralPercentage uint8 `gorm:"default:0;comment:Referral"` // Referral Percentage
OnlyFirstPurchase *bool `gorm:"default:true;not null;comment:Only First Purchase"` // Only First Purchase Referral
GiftAmount int64 `gorm:"default:0;comment:User Gift Amount"`
Enable *bool `gorm:"default:true;not null;comment:Is Account Enabled"`
IsAdmin *bool `gorm:"default:false;not null;comment:Is Admin"`
EnableBalanceNotify *bool `gorm:"default:false;not null;comment:Enable Balance Change Notifications"`
EnableLoginNotify *bool `gorm:"default:false;not null;comment:Enable Login Notifications"`
EnableSubscribeNotify *bool `gorm:"default:false;not null;comment:Enable Subscription Notifications"`
EnableTradeNotify *bool `gorm:"default:false;not null;comment:Enable Trade Notifications"`
AuthMethods []AuthMethods `gorm:"foreignKey:UserId;references:Id"`
UserDevices []Device `gorm:"foreignKey:UserId;references:Id"`
Rules string `gorm:"type:TEXT;comment:User Rules"`
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
UpdatedAt time.Time `gorm:"comment:Update Time"`
DeletedAt gorm.DeletedAt `gorm:"index;comment:Deletion Time"`
}
func (*User) TableName() string {
@ -49,7 +52,6 @@ type Subscribe struct {
Token string `gorm:"index:idx_token;unique;type:varchar(255);default:'';comment:Token"`
UUID string `gorm:"type:varchar(255);unique;index:idx_uuid;default:'';comment:UUID"`
Status uint8 `gorm:"type:tinyint(1);default:0;comment:Subscription Status: 0: Pending 1: Active 2: Finished 3: Expired 4: Deducted"`
Note string `gorm:"type:varchar(500);default:'';comment:User note for subscription"`
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
UpdatedAt time.Time `gorm:"comment:Update Time"`
}

View File

@ -97,7 +97,7 @@ func NewServiceContext(c config.Config) *ServiceContext {
Redis: rds,
Config: c,
Queue: NewAsynqClient(c),
ExchangeRate: 1.0,
ExchangeRate: 0,
GeoIP: geoIP,
//NodeCache: cache.NewNodeCacheClient(rds),
AuthLimiter: authLimiter,

View File

@ -2,9 +2,11 @@ package types
type (
SubscribeRequest struct {
Flag string
Token string
UA string
Flag string
Token string
Type string
UA string
Params map[string]string
}
SubscribeResponse struct {
Config []byte

View File

@ -394,26 +394,27 @@ type CreateSubscribeGroupRequest struct {
}
type CreateSubscribeRequest struct {
Name string `json:"name" validate:"required"`
Language string `json:"language"`
Description string `json:"description"`
UnitPrice int64 `json:"unit_price"`
UnitTime string `json:"unit_time"`
Discount []SubscribeDiscount `json:"discount"`
Replacement int64 `json:"replacement"`
Inventory int64 `json:"inventory"`
Traffic int64 `json:"traffic"`
SpeedLimit int64 `json:"speed_limit"`
DeviceLimit int64 `json:"device_limit"`
Quota int64 `json:"quota"`
Nodes []int64 `json:"nodes"`
NodeTags []string `json:"node_tags"`
Show *bool `json:"show"`
Sell *bool `json:"sell"`
DeductionRatio int64 `json:"deduction_ratio"`
AllowDeduction *bool `json:"allow_deduction"`
ResetCycle int64 `json:"reset_cycle"`
RenewalReset *bool `json:"renewal_reset"`
Name string `json:"name" validate:"required"`
Language string `json:"language"`
Description string `json:"description"`
UnitPrice int64 `json:"unit_price"`
UnitTime string `json:"unit_time"`
Discount []SubscribeDiscount `json:"discount"`
Replacement int64 `json:"replacement"`
Inventory int64 `json:"inventory"`
Traffic int64 `json:"traffic"`
SpeedLimit int64 `json:"speed_limit"`
DeviceLimit int64 `json:"device_limit"`
Quota int64 `json:"quota"`
Nodes []int64 `json:"nodes"`
NodeTags []string `json:"node_tags"`
Show *bool `json:"show"`
Sell *bool `json:"sell"`
DeductionRatio int64 `json:"deduction_ratio"`
AllowDeduction *bool `json:"allow_deduction"`
ResetCycle int64 `json:"reset_cycle"`
RenewalReset *bool `json:"renewal_reset"`
ShowOriginalPrice bool `json:"show_original_price"`
}
type CreateTicketFollowRequest struct {
@ -1054,6 +1055,7 @@ type GetUserListRequest struct {
Size int `form:"size"`
Search string `form:"search,omitempty"`
UserId *int64 `form:"user_id,omitempty"`
Unscoped bool `form:"unscoped,omitempty"`
SubscribeId *int64 `form:"subscribe_id,omitempty"`
UserSubscribeId *int64 `form:"user_subscribe_id,omitempty"`
}
@ -1568,7 +1570,7 @@ type PurchaseOrderResponse struct {
type QueryAnnouncementRequest struct {
Page int `form:"page"`
Size int `form:"size"`
Size int `form:"size,default=15"`
Pinned *bool `form:"pinned"`
Popup *bool `form:"popup"`
}
@ -1856,6 +1858,10 @@ type ResetUserSubscribeTokenRequest struct {
UserSubscribeId int64 `json:"user_subscribe_id"`
}
type ResetUserSubscribeTrafficRequest struct {
UserSubscribeId int64 `json:"user_subscribe_id"`
}
type RevenueStatisticsResponse struct {
Today OrdersStatistics `json:"today"`
Monthly OrdersStatistics `json:"monthly"`
@ -2056,30 +2062,31 @@ type StripePayment struct {
}
type Subscribe struct {
Id int64 `json:"id"`
Name string `json:"name"`
Language string `json:"language"`
Description string `json:"description"`
UnitPrice int64 `json:"unit_price"`
UnitTime string `json:"unit_time"`
Discount []SubscribeDiscount `json:"discount"`
Replacement int64 `json:"replacement"`
Inventory int64 `json:"inventory"`
Traffic int64 `json:"traffic"`
SpeedLimit int64 `json:"speed_limit"`
DeviceLimit int64 `json:"device_limit"`
Quota int64 `json:"quota"`
Nodes []int64 `json:"nodes"`
NodeTags []string `json:"node_tags"`
Show bool `json:"show"`
Sell bool `json:"sell"`
Sort int64 `json:"sort"`
DeductionRatio int64 `json:"deduction_ratio"`
AllowDeduction bool `json:"allow_deduction"`
ResetCycle int64 `json:"reset_cycle"`
RenewalReset bool `json:"renewal_reset"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
Id int64 `json:"id"`
Name string `json:"name"`
Language string `json:"language"`
Description string `json:"description"`
UnitPrice int64 `json:"unit_price"`
UnitTime string `json:"unit_time"`
Discount []SubscribeDiscount `json:"discount"`
Replacement int64 `json:"replacement"`
Inventory int64 `json:"inventory"`
Traffic int64 `json:"traffic"`
SpeedLimit int64 `json:"speed_limit"`
DeviceLimit int64 `json:"device_limit"`
Quota int64 `json:"quota"`
Nodes []int64 `json:"nodes"`
NodeTags []string `json:"node_tags"`
Show bool `json:"show"`
Sell bool `json:"sell"`
Sort int64 `json:"sort"`
DeductionRatio int64 `json:"deduction_ratio"`
AllowDeduction bool `json:"allow_deduction"`
ResetCycle int64 `json:"reset_cycle"`
RenewalReset bool `json:"renewal_reset"`
ShowOriginalPrice bool `json:"show_original_price"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
}
type SubscribeApplication struct {
@ -2239,6 +2246,10 @@ type ToggleNodeStatusRequest struct {
Enable *bool `json:"enable"`
}
type ToggleUserSubscribeStatusRequest struct {
UserSubscribeId int64 `json:"user_subscribe_id"`
}
type TosConfig struct {
TosContent string `json:"tos_content"`
}
@ -2435,28 +2446,29 @@ type UpdateSubscribeGroupRequest struct {
}
type UpdateSubscribeRequest struct {
Id int64 `json:"id" validate:"required"`
Name string `json:"name" validate:"required"`
Language string `json:"language"`
Description string `json:"description"`
UnitPrice int64 `json:"unit_price"`
UnitTime string `json:"unit_time"`
Discount []SubscribeDiscount `json:"discount"`
Replacement int64 `json:"replacement"`
Inventory int64 `json:"inventory"`
Traffic int64 `json:"traffic"`
SpeedLimit int64 `json:"speed_limit"`
DeviceLimit int64 `json:"device_limit"`
Quota int64 `json:"quota"`
Nodes []int64 `json:"nodes"`
NodeTags []string `json:"node_tags"`
Show *bool `json:"show"`
Sell *bool `json:"sell"`
Sort int64 `json:"sort"`
DeductionRatio int64 `json:"deduction_ratio"`
AllowDeduction *bool `json:"allow_deduction"`
ResetCycle int64 `json:"reset_cycle"`
RenewalReset *bool `json:"renewal_reset"`
Id int64 `json:"id" validate:"required"`
Name string `json:"name" validate:"required"`
Language string `json:"language"`
Description string `json:"description"`
UnitPrice int64 `json:"unit_price"`
UnitTime string `json:"unit_time"`
Discount []SubscribeDiscount `json:"discount"`
Replacement int64 `json:"replacement"`
Inventory int64 `json:"inventory"`
Traffic int64 `json:"traffic"`
SpeedLimit int64 `json:"speed_limit"`
DeviceLimit int64 `json:"device_limit"`
Quota int64 `json:"quota"`
Nodes []int64 `json:"nodes"`
NodeTags []string `json:"node_tags"`
Show *bool `json:"show"`
Sell *bool `json:"sell"`
Sort int64 `json:"sort"`
DeductionRatio int64 `json:"deduction_ratio"`
AllowDeduction *bool `json:"allow_deduction"`
ResetCycle int64 `json:"reset_cycle"`
RenewalReset *bool `json:"renewal_reset"`
ShowOriginalPrice bool `json:"show_original_price"`
}
type UpdateTicketStatusRequest struct {
@ -2551,7 +2563,6 @@ type User struct {
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
DeletedAt int64 `json:"deleted_at,omitempty"`
IsDel bool `json:"is_del,omitempty"`
}
type UserAffiliate struct {

View File

@ -32,15 +32,15 @@ func (l *GormLogger) LogMode(logger.LogLevel) logger.Interface {
}
func (l *GormLogger) Info(ctx context.Context, str string, args ...interface{}) {
WithContext(ctx).WithCallerSkip(6).Infof("%s Info: %s", TAG, str, args)
WithContext(ctx).WithCallerSkip(2).Infof("%s Info: %s", TAG, str, args)
}
func (l *GormLogger) Warn(ctx context.Context, str string, args ...interface{}) {
WithContext(ctx).WithCallerSkip(6).Infof("%s Warn: %s", TAG, str, args)
WithContext(ctx).WithCallerSkip(2).Infof("%s Warn: %s", TAG, str, args)
}
func (l *GormLogger) Error(ctx context.Context, str string, args ...interface{}) {
WithContext(ctx).WithCallerSkip(6).Errorf("%s Error: %s", TAG, str, args)
WithContext(ctx).WithCallerSkip(2).Errorf("%s Error: %s", TAG, str, args)
}
func (l *GormLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) {

View File

@ -71,6 +71,7 @@ const (
SubscribeIsUsedError uint32 = 60004
SingleSubscribeModeExceedsLimit uint32 = 60005
SubscribeQuotaLimit uint32 = 60006
SubscribeOutOfStock uint32 = 60007
)
// Auth error

View File

@ -56,6 +56,7 @@ func init() {
SubscribeIsUsedError: "Subscribe is used",
SingleSubscribeModeExceedsLimit: "Single subscribe mode exceeds limit",
SubscribeQuotaLimit: "Subscribe quota limit",
SubscribeOutOfStock: "Subscribe out of stock",
// auth error
VerifyCodeError: "Verify code error",

View File

@ -79,6 +79,7 @@ func (l *TrafficStatisticsLogic) ProcessTask(ctx context.Context, task *asynq.Ta
now := time.Now()
realTimeMultiplier := l.svc.NodeMultiplierManager.GetMultiplier(now)
logger.Debugf("[TrafficStatisticsLogic] Current time traffic multiplier: %.2f", realTimeMultiplier)
for _, log := range payload.Logs {
// query user Subscribe Info
sub, err := l.svc.UserModel.FindOneSubscribe(ctx, log.SID)