init
This commit is contained in:
commit
e898e6afbe
@ -89,16 +89,20 @@ type (
|
||||
CreateRuleGroupRequest {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Icon string `json:"icon"`
|
||||
Type string `json:"type"`
|
||||
Tags []string `json:"tags"`
|
||||
Rules string `json:"rules"`
|
||||
Default bool `json:"default"`
|
||||
Enable bool `json:"enable"`
|
||||
}
|
||||
UpdateRuleGroupRequest {
|
||||
Id int64 `json:"id" validate:"required"`
|
||||
Icon string `json:"icon"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Tags []string `json:"tags"`
|
||||
Rules string `json:"rules"`
|
||||
Default bool `json:"default"`
|
||||
Enable bool `json:"enable"`
|
||||
}
|
||||
DeleteRuleGroupRequest {
|
||||
|
||||
@ -154,8 +154,9 @@ type (
|
||||
}
|
||||
InviteConfig {
|
||||
ForcedInvite bool `json:"forced_invite"`
|
||||
ReferralPercentage int64 `json:"referral_percentage"`
|
||||
OnlyFirstPurchase bool `json:"only_first_purchase"`
|
||||
FirstPurchasePercentage int64 `json:"first_purchase_percentage"`
|
||||
FirstYearlyPurchasePercentage int64 `json:"first_yearly_purchase_percentage"`
|
||||
NonFirstPurchasePercentage int64 `json:"non_first_purchase_percentage"`
|
||||
}
|
||||
TelegramConfig {
|
||||
TelegramBotToken string `json:"telegram_bot_token"`
|
||||
@ -515,9 +516,11 @@ type (
|
||||
Id int64 `json:"id"`
|
||||
Icon string `json:"icon"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Type string `json:"type"`
|
||||
Tags []string `json:"tags"`
|
||||
Rules string `json:"rules"`
|
||||
Enable bool `json:"enable"`
|
||||
Default bool `json:"default"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
Host: 0.0.0.0
|
||||
Port: 8080
|
||||
<<<<<<< HEAD
|
||||
TLS:
|
||||
Enable: false
|
||||
CertFile: ""
|
||||
@ -37,3 +38,41 @@ Redis:
|
||||
Host: redis:6379
|
||||
Pass:
|
||||
DB: 0
|
||||
=======
|
||||
Debug: false
|
||||
JwtAuth:
|
||||
AccessSecret: 1234567890
|
||||
AccessExpire: 604800
|
||||
Logger:
|
||||
ServiceName: PPanel
|
||||
Mode: console
|
||||
Encoding: plain
|
||||
TimeFormat: '2025-01-01 00:00:00.000'
|
||||
Path: logs
|
||||
Level: debug
|
||||
MaxContentLength: 0
|
||||
Compress: false
|
||||
Stat: true
|
||||
KeepDays: 0
|
||||
StackCooldownMillis: 100
|
||||
MaxBackups: 0
|
||||
MaxSize: 0
|
||||
Rotation: daily
|
||||
FileTimeFormat: 2025-01-01T00:00:00.000Z00:00
|
||||
MySQL:
|
||||
Addr: 172.245.180.199:3306
|
||||
Dbname: ppanel
|
||||
Username: ppanel
|
||||
Password: ppanelpassword
|
||||
Config: charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
|
||||
MaxIdleConns: 10
|
||||
MaxOpenConns: 10
|
||||
SlowThreshold: 1000
|
||||
Redis:
|
||||
Host: ppanel-cache:6379
|
||||
Pass:
|
||||
DB: 0
|
||||
Administrator:
|
||||
Password: password
|
||||
Email: admin@ppanel.dev
|
||||
>>>>>>> old
|
||||
|
||||
8
go.mod
8
go.mod
@ -56,6 +56,7 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Masterminds/sprig/v3 v3.3.0
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/goccy/go-json v0.10.4
|
||||
github.com/golang-migrate/migrate/v4 v4.18.2
|
||||
@ -66,7 +67,10 @@ require (
|
||||
|
||||
require (
|
||||
cloud.google.com/go/compute/metadata v0.6.0 // indirect
|
||||
dario.cat/mergo v1.0.1 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.3.0 // indirect
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
|
||||
github.com/alibabacloud-go/debug v1.0.1 // indirect
|
||||
github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect
|
||||
@ -99,6 +103,7 @@ require (
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
@ -107,12 +112,15 @@ require (
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/openzipkin/zipkin-go v0.4.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/smartwalle/ncrypto v1.0.4 // indirect
|
||||
github.com/smartwalle/ngx v1.0.9 // indirect
|
||||
github.com/smartwalle/nsign v1.0.9 // indirect
|
||||
|
||||
16
go.sum
16
go.sum
@ -1,6 +1,8 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
|
||||
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
|
||||
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
||||
@ -8,6 +10,12 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg6
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/GUAIK-ORG/go-snowflake v0.0.0-20200116064823-220c4260e85f h1:RDkg3pyE1qGbBpRWmvSN9RNZC5nUrOaEPiEpEb8y2f0=
|
||||
github.com/GUAIK-ORG/go-snowflake v0.0.0-20200116064823-220c4260e85f/go.mod h1:zA7AF9RTfpluCfz0omI4t5KCMaWHUMicsZoMccnaT44=
|
||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||
github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
|
||||
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
|
||||
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc=
|
||||
@ -208,6 +216,8 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hibiken/asynq v0.24.1 h1:+5iIEAyA9K/lcSPvx3qoPtsKJeKI5u9aOIvUmSsazEw=
|
||||
github.com/hibiken/asynq v0.24.1/go.mod h1:u5qVeSbrnfT+vtG5Mq8ZPzQu/BmCKMHvTGb91uy9Tts=
|
||||
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
|
||||
@ -249,6 +259,10 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
|
||||
github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||
@ -287,6 +301,8 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/smartwalle/alipay/v3 v3.2.23 h1:i1VwJeu70EmwpsXXz6GZZnMAtRx5MTfn2dPoql/L3zE=
|
||||
github.com/smartwalle/alipay/v3 v3.2.23/go.mod h1:lVqFiupPf8YsAXaq5JXcwqnOUC2MCF+2/5vub+RlagE=
|
||||
github.com/smartwalle/ncrypto v1.0.4 h1:P2rqQxDepJwgeO5ShoC+wGcK2wNJDmcdBOWAksuIgx8=
|
||||
|
||||
@ -17,13 +17,11 @@ func Email(ctx *svc.ServiceContext) {
|
||||
logger.Debug("Email config initialization")
|
||||
method, err := ctx.AuthModel.FindOneByMethod(context.Background(), "email")
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to find email auth method: %v", err.Error()))
|
||||
panic(fmt.Sprintf("[Error] Initialization Failed to find email auth method: %v", err.Error()))
|
||||
}
|
||||
var cfg config.EmailConfig
|
||||
var emailConfig = new(auth.EmailAuthConfig)
|
||||
if err := emailConfig.Unmarshal(method.Config); err != nil {
|
||||
panic(fmt.Sprintf("failed to unmarshal email auth config: %v", err.Error()))
|
||||
}
|
||||
emailConfig.Unmarshal(method.Config)
|
||||
tool.DeepCopy(&cfg, emailConfig)
|
||||
cfg.Enable = *method.Enabled
|
||||
value, _ := json.Marshal(emailConfig.PlatformConfig)
|
||||
|
||||
@ -37,7 +37,7 @@ VALUES (1, 'Clash', 'Clash', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648
|
||||
(6, 'Netch', 'Netch', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
|
||||
(7, 'Quantumult', 'Quantumult', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
|
||||
(8, 'Shadowrocket', 'Shadowrocket', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
|
||||
(9, 'Singhandle', 'Singhandle', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
|
||||
(9, 'SingBox', ' SingBox', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
|
||||
(10, 'Surfboard', 'Surfboard', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
|
||||
(11, 'Surge', 'Surge', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
|
||||
(12, 'V2box', 'V2box', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
|
||||
@ -90,11 +90,15 @@ VALUES (1, 'site', 'SiteLogo', '/favicon.svg', 'string', 'Site Logo', '2025-04-2
|
||||
'2025-04-22 14:25:16.640'),
|
||||
(23, 'invite', 'ForcedInvite', 'false', 'bool', 'Forced invite', '2025-04-22 14:25:16.640',
|
||||
'2025-04-22 14:25:16.640'),
|
||||
(24, 'invite', 'ReferralPercentage', '20', 'int', 'Referral percentage', '2025-04-22 14:25:16.640',
|
||||
(24, 'invite', 'FirstPurchasePercentage', '20', 'int', 'First purchase commission percentage', '2025-04-22 14:25:16.640',
|
||||
'2025-04-22 14:25:16.640'),
|
||||
(25, 'invite', 'OnlyFirstPurchase', 'false', 'bool', 'Only first purchase', '2025-04-22 14:25:16.640',
|
||||
(25, 'invite', 'NonFirstPurchasePercentage', '10', 'int', 'Non-first purchase commission percentage', '2025-04-22 14:25:16.640',
|
||||
'2025-04-22 14:25:16.640'),
|
||||
(26, 'register', 'StopRegister', 'false', 'bool', 'is stop register', '2025-04-22 14:25:16.640',
|
||||
(26, 'invite', 'ForcedInvite', 'false', 'bool', 'Forced invite', '2025-04-22 14:25:16.640',
|
||||
'2025-04-22 14:25:16.640'),
|
||||
(42, 'invite', 'FirstYearlyPurchasePercentage', '25', 'int', 'First yearly purchase commission percentage', '2025-04-22 14:25:16.640',
|
||||
'2025-04-22 14:25:16.640'),
|
||||
(27, 'register', 'StopRegister', 'false', 'bool', 'is stop register', '2025-04-22 14:25:16.640',
|
||||
'2025-04-22 14:25:16.640'),
|
||||
(27, 'register', 'EnableTrial', 'false', 'bool', 'is enable trial', '2025-04-22 14:25:16.640',
|
||||
'2025-04-22 14:25:16.640'),
|
||||
|
||||
3
initialize/migrate/database/02007_adapte_rule.down.sql
Normal file
3
initialize/migrate/database/02007_adapte_rule.down.sql
Normal file
@ -0,0 +1,3 @@
|
||||
ALTER TABLE `server_rule_group`
|
||||
DROP COLUMN `default`,
|
||||
DROP COLUMN `type`;
|
||||
3
initialize/migrate/database/02007_adapte_rule.up.sql
Normal file
3
initialize/migrate/database/02007_adapte_rule.up.sql
Normal file
@ -0,0 +1,3 @@
|
||||
ALTER TABLE `server_rule_group`
|
||||
ADD COLUMN `default` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Is Default Group',
|
||||
ADD COLUMN `type` VARCHAR(100) NOT NULL DEFAULT '' COMMENT 'Rule Group Type';
|
||||
@ -3,7 +3,6 @@ package initialize
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
|
||||
@ -21,9 +20,7 @@ func Mobile(ctx *svc.ServiceContext) {
|
||||
}
|
||||
var cfg config.MobileConfig
|
||||
var mobileConfig auth.MobileAuthConfig
|
||||
if err := mobileConfig.Unmarshal(method.Config); err != nil {
|
||||
panic(fmt.Sprintf("failed to unmarshal mobile auth config: %v", err.Error()))
|
||||
}
|
||||
mobileConfig.Unmarshal(method.Config)
|
||||
tool.DeepCopy(&cfg, mobileConfig)
|
||||
cfg.Enable = *method.Enabled
|
||||
value, _ := json.Marshal(mobileConfig.PlatformConfig)
|
||||
|
||||
@ -120,8 +120,9 @@ type File struct {
|
||||
|
||||
type InviteConfig struct {
|
||||
ForcedInvite bool `yaml:"ForcedInvite" default:"false"`
|
||||
ReferralPercentage int64 `yaml:"ReferralPercentage" default:"0"`
|
||||
OnlyFirstPurchase bool `yaml:"OnlyFirstPurchase" default:"false"`
|
||||
FirstPurchasePercentage int64 `yaml:"FirstPurchasePercentage" default:"20"`
|
||||
FirstYearlyPurchasePercentage int64 `yaml:"FirstYearlyPurchasePercentage" default:"25"`
|
||||
NonFirstPurchasePercentage int64 `yaml:"NonFirstPurchasePercentage" default:"10"`
|
||||
}
|
||||
|
||||
type Telegram struct {
|
||||
|
||||
@ -8,7 +8,7 @@ import (
|
||||
"github.com/perfect-panel/server/pkg/result"
|
||||
)
|
||||
|
||||
// Create rule group
|
||||
// CreateRuleGroupHandler Create rule group
|
||||
func CreateRuleGroupHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
var req types.CreateRuleGroupRequest
|
||||
|
||||
@ -18,7 +18,7 @@ type GetAuthMethodConfigLogic struct {
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
// Get auth method config
|
||||
// NewGetAuthMethodConfigLogic Get auth method config
|
||||
func NewGetAuthMethodConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAuthMethodConfigLogic {
|
||||
return &GetAuthMethodConfigLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
|
||||
@ -40,34 +40,32 @@ func (l *UpdateAuthMethodConfigLogic) UpdateAuthMethodConfig(req *types.UpdateAu
|
||||
|
||||
tool.DeepCopy(method, req)
|
||||
if req.Config != nil {
|
||||
if value, ok := req.Config.(map[string]interface{}); ok {
|
||||
if req.Method == "email" && value["verify_email_template"] == "" {
|
||||
value["verify_email_template"] = email.DefaultEmailVerifyTemplate
|
||||
_, exist := req.Config.(map[string]interface{})
|
||||
if !exist {
|
||||
req.Config = initializePlatformConfig(req.Method).(string)
|
||||
}
|
||||
if req.Method == "email" && value["expiration_email_template"] == "" {
|
||||
value["expiration_email_template"] = email.DefaultExpirationEmailTemplate
|
||||
}
|
||||
if req.Method == "email" && value["maintenance_email_template"] == "" {
|
||||
value["maintenance_email_template"] = email.DefaultMaintenanceEmailTemplate
|
||||
}
|
||||
if req.Method == "email" && value["traffic_exceed_email_template"] == "" {
|
||||
value["traffic_exceed_email_template"] = email.DefaultTrafficExceedEmailTemplate
|
||||
if req.Method == "email" {
|
||||
configs, _ := json.Marshal(req.Config)
|
||||
emailConfig := new(auth.EmailAuthConfig)
|
||||
emailConfig.Unmarshal(string(configs))
|
||||
req.Config = emailConfig
|
||||
}
|
||||
|
||||
if value["platform_config"] != nil {
|
||||
platformConfig, err := validatePlatformConfig(value["platform"].(string), value["platform_config"].(map[string]interface{}))
|
||||
if err != nil {
|
||||
l.Errorw("validate platform config failed", logger.Field("config", req.Config), logger.Field("error", err.Error()))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "validate platform config failed: %v", err.Error())
|
||||
}
|
||||
req.Config.(map[string]interface{})["platform_config"] = platformConfig
|
||||
}
|
||||
if req.Method == "mobile" {
|
||||
configs, _ := json.Marshal(req.Config)
|
||||
mobileConfig := new(auth.MobileAuthConfig)
|
||||
mobileConfig.Unmarshal(string(configs))
|
||||
req.Config = mobileConfig
|
||||
}
|
||||
|
||||
bytes, err := json.Marshal(req.Config)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "marshal config failed: %v", err.Error())
|
||||
}
|
||||
method.Config = string(bytes)
|
||||
} else {
|
||||
// initialize platform config
|
||||
method.Config = initializePlatformConfig(req.Method).(string)
|
||||
}
|
||||
err = l.svcCtx.AuthModel.Update(l.ctx, method)
|
||||
if err != nil {
|
||||
@ -124,3 +122,26 @@ func validatePlatformConfig(platform string, cfg map[string]interface{}) (interf
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func initializePlatformConfig(platform string) interface{} {
|
||||
var result interface{}
|
||||
switch platform {
|
||||
case "email":
|
||||
result = new(auth.EmailAuthConfig).Marshal()
|
||||
case "mobile":
|
||||
result = new(auth.MobileAuthConfig).Marshal()
|
||||
case "apple":
|
||||
result = new(auth.AppleAuthConfig).Marshal()
|
||||
case "google":
|
||||
result = new(auth.GoogleAuthConfig).Marshal()
|
||||
case "github":
|
||||
result = new(auth.GithubAuthConfig).Marshal()
|
||||
case "facebook":
|
||||
result = new(auth.FacebookAuthConfig).Marshal()
|
||||
case "telegram":
|
||||
result = new(auth.TelegramAuthConfig).Marshal()
|
||||
case "device":
|
||||
result = new(auth.DeviceConfig).Marshal()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@ -53,17 +53,26 @@ func (l *CreateRuleGroupLogic) CreateRuleGroup(req *types.CreateRuleGroupRequest
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = l.svcCtx.ServerModel.InsertRuleGroup(l.ctx, &server.RuleGroup{
|
||||
info := &server.RuleGroup{
|
||||
Name: req.Name,
|
||||
Icon: req.Icon,
|
||||
Type: req.Type,
|
||||
Tags: tool.StringSliceToString(req.Tags),
|
||||
Rules: strings.Join(rs, "\n"),
|
||||
Default: req.Default,
|
||||
Enable: req.Enable,
|
||||
})
|
||||
}
|
||||
err = l.svcCtx.ServerModel.InsertRuleGroup(l.ctx, info)
|
||||
if err != nil {
|
||||
l.Errorw("[CreateRuleGroup] Insert Database Error: ", logger.Field("error", err.Error()))
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create server rule group error: %v", err)
|
||||
}
|
||||
if req.Default {
|
||||
if err = l.svcCtx.ServerModel.SetDefaultRuleGroup(l.ctx, info.Id); err != nil {
|
||||
l.Errorw("[CreateRuleGroup] Set Default Rule Group Error: ", logger.Field("error", err.Error()))
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "set default rule group error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -38,9 +38,11 @@ func (l *GetRuleGroupListLogic) GetRuleGroupList() (resp *types.GetRuleGroupResp
|
||||
Id: v.Id,
|
||||
Icon: v.Icon,
|
||||
Name: v.Name,
|
||||
Type: v.Type,
|
||||
Tags: strings.Split(v.Tags, ","),
|
||||
Rules: v.Rules,
|
||||
Enable: v.Enable,
|
||||
Default: v.Default,
|
||||
CreatedAt: v.CreatedAt.UnixMilli(),
|
||||
UpdatedAt: v.UpdatedAt.UnixMilli(),
|
||||
}
|
||||
|
||||
@ -38,13 +38,21 @@ func (l *UpdateRuleGroupLogic) UpdateRuleGroup(req *types.UpdateRuleGroupRequest
|
||||
err = l.svcCtx.ServerModel.UpdateRuleGroup(l.ctx, &server.RuleGroup{
|
||||
Id: req.Id,
|
||||
Icon: req.Icon,
|
||||
Type: req.Type,
|
||||
Name: req.Name,
|
||||
Tags: tool.StringSliceToString(req.Tags),
|
||||
Rules: strings.Join(rs, "\n"),
|
||||
Default: req.Default,
|
||||
Enable: req.Enable,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), err.Error())
|
||||
}
|
||||
if req.Default {
|
||||
if err = l.svcCtx.ServerModel.SetDefaultRuleGroup(l.ctx, req.Id); err != nil {
|
||||
l.Errorf("SetDefaultRuleGroup error: %v", err.Error())
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), err.Error())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -44,6 +44,7 @@ func (l *UpdateUserBasicInfoLogic) UpdateUserBasicInfo(req *types.UpdateUserBasi
|
||||
userInfo.Balance = req.Balance
|
||||
userInfo.GiftAmount = req.GiftAmount
|
||||
userInfo.Commission = req.Commission
|
||||
// 手动设置 IsAdmin 字段,因为类型不匹配(*bool vs bool)
|
||||
userInfo.IsAdmin = &req.IsAdmin
|
||||
userInfo.Enable = &req.Enable
|
||||
|
||||
|
||||
@ -33,8 +33,15 @@ func (l *UpdateUserSubscribeLogic) UpdateUserSubscribe(req *types.UpdateUserSubs
|
||||
l.Errorw("FindOneUserSubscribe failed:", logger.Field("error", err.Error()), logger.Field("userSubscribeId", req.UserSubscribeId))
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindOneUserSubscribe failed: %v", err.Error())
|
||||
}
|
||||
expiredAt := time.UnixMilli(req.ExpiredAt)
|
||||
if time.Since(expiredAt).Minutes() > 0 {
|
||||
userSub.Status = 3
|
||||
} else {
|
||||
userSub.Status = 1
|
||||
}
|
||||
|
||||
err = l.svcCtx.UserModel.UpdateSubscribe(l.ctx, &user.Subscribe{
|
||||
Id: req.UserSubscribeId,
|
||||
Id: userSub.Id,
|
||||
UserId: userSub.UserId,
|
||||
OrderId: userSub.OrderId,
|
||||
SubscribeId: req.SubscribeId,
|
||||
|
||||
@ -124,6 +124,7 @@ func (l *GetStatLogic) GetStat() (resp *types.GetStatResponse, err error) {
|
||||
Node: n,
|
||||
Country: int64(len(country)),
|
||||
Protocol: protocol,
|
||||
OnlineDevice: l.svcCtx.DeviceManager.GetOnlineDeviceCount(),
|
||||
}
|
||||
val, _ := json.Marshal(*resp)
|
||||
_ = l.svcCtx.Redis.Set(l.ctx, config.CommonStatCacheKey, string(val), time.Duration(3600)*time.Second).Err()
|
||||
|
||||
@ -82,7 +82,7 @@ func (l *CloseOrderLogic) CloseOrder(req *types.CloseOrderRequest) error {
|
||||
return err
|
||||
}
|
||||
deduction := userInfo.GiftAmount + orderInfo.GiftAmount
|
||||
err = tx.Model(&user.User{}).Where("id = ?", orderInfo.UserId).Update("deduction", deduction).Error
|
||||
err = tx.Model(&user.User{}).Where("id = ?", orderInfo.UserId).Update("gift_amount", deduction).Error
|
||||
if err != nil {
|
||||
l.Errorw("[CloseOrder] Refund deduction amount failed",
|
||||
logger.Field("error", err.Error()),
|
||||
|
||||
@ -10,5 +10,6 @@ func getDiscount(discounts []types.SubscribeDiscount, inputMonths int64) float64
|
||||
finalDiscount = discount.Discount
|
||||
}
|
||||
}
|
||||
|
||||
return float64(finalDiscount) / float64(100)
|
||||
}
|
||||
|
||||
@ -24,7 +24,8 @@ type PreCreateOrderLogic struct {
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
// Pre create order
|
||||
// NewPreCreateOrderLogic creates a new pre-create order logic instance for order preview operations.
|
||||
// It initializes the logger with context and sets up the service context for database operations.
|
||||
func NewPreCreateOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PreCreateOrderLogic {
|
||||
return &PreCreateOrderLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
@ -33,12 +34,21 @@ func NewPreCreateOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Pr
|
||||
}
|
||||
}
|
||||
|
||||
// PreCreateOrder calculates order pricing preview including discounts, coupons, gift amounts, and fees
|
||||
// without actually creating an order. It validates subscription plans, coupons, and payment methods
|
||||
// to provide accurate pricing information for the frontend order preview.
|
||||
func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (resp *types.PreOrderResponse, err error) {
|
||||
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
||||
if !ok {
|
||||
logger.Error("current user is not found in context")
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
||||
}
|
||||
|
||||
if req.Quantity <= 0 {
|
||||
l.Debugf("[PreCreateOrder] Quantity is less than or equal to 0, setting to 1")
|
||||
req.Quantity = 1
|
||||
}
|
||||
|
||||
// find subscribe plan
|
||||
sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, req.SubscribeId)
|
||||
if err != nil {
|
||||
@ -52,6 +62,7 @@ func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (r
|
||||
discount = getDiscount(dis, req.Quantity)
|
||||
}
|
||||
price := sub.UnitPrice * req.Quantity
|
||||
|
||||
amount := int64(float64(price) * discount)
|
||||
discountAmount := price - amount
|
||||
var couponAmount int64
|
||||
@ -72,7 +83,7 @@ func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (r
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
l.Logger.Error("[PreCreateOrder] Database query error", logger.Field("error", err.Error()), logger.Field("user_id", u.Id), logger.Field("coupon", req.Coupon))
|
||||
l.Errorw("[PreCreateOrder] Database query error", logger.Field("error", err.Error()), logger.Field("user_id", u.Id), logger.Field("coupon", req.Coupon))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find coupon error: %v", err.Error())
|
||||
}
|
||||
|
||||
@ -103,7 +114,7 @@ func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (r
|
||||
if req.Payment != 0 {
|
||||
payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment)
|
||||
if err != nil {
|
||||
l.Logger.Error("[PreCreateOrder] Database query error", logger.Field("error", err.Error()), logger.Field("payment", req.Payment))
|
||||
l.Errorw("[PreCreateOrder] Database query error", logger.Field("error", err.Error()), logger.Field("payment", req.Payment))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find payment method error: %v", err.Error())
|
||||
}
|
||||
// Calculate the handling fee
|
||||
|
||||
@ -31,7 +31,8 @@ const (
|
||||
CloseOrderTimeMinutes = 15
|
||||
)
|
||||
|
||||
// NewPurchaseLogic purchase Subscription
|
||||
// NewPurchaseLogic creates a new purchase logic instance for subscription purchase operations.
|
||||
// It initializes the logger with context and sets up the service context for database operations.
|
||||
func NewPurchaseLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PurchaseLogic {
|
||||
return &PurchaseLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
@ -40,6 +41,9 @@ func NewPurchaseLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Purchase
|
||||
}
|
||||
}
|
||||
|
||||
// Purchase processes new subscription purchase orders including validation, discount calculation,
|
||||
// coupon processing, gift amount deduction, fee calculation, and order creation with database transaction.
|
||||
// It handles the complete purchase workflow from user validation to order creation and task scheduling.
|
||||
func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.PurchaseOrderResponse, err error) {
|
||||
|
||||
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
||||
@ -47,6 +51,12 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
|
||||
logger.Error("current user is not found in context")
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
||||
}
|
||||
|
||||
if req.Quantity <= 0 {
|
||||
l.Debugf("[Purchase] Quantity is less than or equal to 0, setting to 1")
|
||||
req.Quantity = 1
|
||||
}
|
||||
|
||||
// find user subscription
|
||||
userSub, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, u.Id)
|
||||
if err != nil {
|
||||
@ -142,7 +152,7 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
|
||||
// find payment method
|
||||
payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment)
|
||||
if err != nil {
|
||||
l.Logger.Error("[Purchase] Database query error", logger.Field("error", err.Error()), logger.Field("payment", req.Payment))
|
||||
l.Errorw("[Purchase] Database query error", logger.Field("error", err.Error()), logger.Field("payment", req.Payment))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find payment method error: %v", err.Error())
|
||||
}
|
||||
var feeAmount int64
|
||||
@ -180,8 +190,8 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
|
||||
// update user deduction && Pre deduction ,Return after canceling the order
|
||||
if orderInfo.GiftAmount > 0 {
|
||||
// update user deduction && Pre deduction ,Return after canceling the order
|
||||
if e := l.svcCtx.UserModel.Update(l.ctx, u, db); err != nil {
|
||||
l.Errorw("[Purchase] Database update error", logger.Field("error", err.Error()), logger.Field("user", u))
|
||||
if e := l.svcCtx.UserModel.Update(l.ctx, u, db); e != nil {
|
||||
l.Errorw("[Purchase] Database update error", logger.Field("error", e.Error()), logger.Field("user", u))
|
||||
return e
|
||||
}
|
||||
// create deduction record
|
||||
@ -195,7 +205,7 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
|
||||
}
|
||||
if e := db.Model(&user.GiftAmountLog{}).Create(&giftAmountLog).Error; e != nil {
|
||||
l.Errorw("[Purchase] Database insert error",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("error", e.Error()),
|
||||
logger.Field("deductionLog", giftAmountLog),
|
||||
)
|
||||
return e
|
||||
@ -213,14 +223,14 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
|
||||
}
|
||||
val, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
l.Errorw("[CreateOrder] Marshal payload error", logger.Field("error", err.Error()), logger.Field("payload", payload))
|
||||
l.Errorw("[Purchase] Marshal payload error", logger.Field("error", err.Error()), logger.Field("payload", payload))
|
||||
}
|
||||
task := asynq.NewTask(queue.DeferCloseOrder, val, asynq.MaxRetry(3))
|
||||
taskInfo, err := l.svcCtx.Queue.Enqueue(task, asynq.ProcessIn(CloseOrderTimeMinutes*time.Minute))
|
||||
if err != nil {
|
||||
l.Errorw("[CreateOrder] Enqueue task error", logger.Field("error", err.Error()), logger.Field("task", task))
|
||||
l.Errorw("[Purchase] Enqueue task error", logger.Field("error", err.Error()), logger.Field("task", task))
|
||||
} else {
|
||||
l.Infow("[CreateOrder] Enqueue task success", logger.Field("TaskID", taskInfo.ID))
|
||||
l.Infow("[Purchase] Enqueue task success", logger.Field("TaskID", taskInfo.ID))
|
||||
}
|
||||
|
||||
return &types.PurchaseOrderResponse{
|
||||
|
||||
@ -27,7 +27,7 @@ type RenewalLogic struct {
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
// Renewal Subscription
|
||||
// NewRenewalLogic creates a new renewal logic instance for subscription renewal operations
|
||||
func NewRenewalLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RenewalLogic {
|
||||
return &RenewalLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
@ -36,12 +36,19 @@ func NewRenewalLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RenewalLo
|
||||
}
|
||||
}
|
||||
|
||||
// Renewal processes subscription renewal orders including discount calculation,
|
||||
// coupon validation, gift amount deduction, fee calculation, and order creation
|
||||
func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.RenewalOrderResponse, err error) {
|
||||
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
||||
if !ok {
|
||||
logger.Error("current user is not found in context")
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
||||
}
|
||||
if req.Quantity <= 0 {
|
||||
l.Debugf("[Renewal] Quantity is less than or equal to 0, setting to 1")
|
||||
req.Quantity = 1
|
||||
}
|
||||
|
||||
orderNo := tool.GenerateTradeNo()
|
||||
// find user subscribe
|
||||
userSubscribe, err := l.svcCtx.UserModel.FindOneUserSubscribe(l.ctx, req.UserSubscribeID)
|
||||
@ -100,7 +107,7 @@ func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.Rene
|
||||
payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment)
|
||||
if err != nil {
|
||||
l.Errorw("[Renewal] Database query error", logger.Field("error", err.Error()), logger.Field("payment", req.Payment))
|
||||
return nil, errors.Wrapf(err, "find payment error: %v", err.Error())
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find payment error: %v", err.Error())
|
||||
}
|
||||
amount -= coupon
|
||||
|
||||
@ -109,8 +116,8 @@ func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.Rene
|
||||
if u.GiftAmount > 0 {
|
||||
if u.GiftAmount >= amount {
|
||||
deductionAmount = amount
|
||||
u.GiftAmount -= deductionAmount
|
||||
amount = 0
|
||||
u.GiftAmount -= amount
|
||||
} else {
|
||||
deductionAmount = u.GiftAmount
|
||||
amount -= u.GiftAmount
|
||||
@ -152,7 +159,7 @@ func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.Rene
|
||||
if orderInfo.GiftAmount > 0 {
|
||||
// update user deduction && Pre deduction ,Return after canceling the order
|
||||
if err := l.svcCtx.UserModel.Update(l.ctx, u, db); err != nil {
|
||||
l.Errorw("[Purchase] Database update error", logger.Field("error", err.Error()), logger.Field("user", u))
|
||||
l.Errorw("[Renewal] Database update error", logger.Field("error", err.Error()), logger.Field("user", u))
|
||||
return err
|
||||
}
|
||||
// create deduction record
|
||||
|
||||
@ -43,10 +43,26 @@ func (l *GetSubscriptionLogic) GetSubscription() (resp *types.GetSubscriptionRes
|
||||
tool.DeepCopy(&sub, item)
|
||||
if item.Discount != "" {
|
||||
var discount []types.SubscribeDiscount
|
||||
|
||||
_ = json.Unmarshal([]byte(item.Discount), &discount)
|
||||
sub.Discount = discount
|
||||
list[i] = sub
|
||||
}
|
||||
|
||||
// 计算节点数量(通过服务组查询关联的实际节点数量)
|
||||
if item.ServerGroup != "" {
|
||||
// 获取服务组ID列表
|
||||
groupIds := tool.StringToInt64Slice(item.ServerGroup)
|
||||
|
||||
// 通过服务组查询关联的节点数量
|
||||
servers, err := l.svcCtx.ServerModel.FindServerListByGroupIds(l.ctx, groupIds)
|
||||
if err != nil {
|
||||
l.Errorw("[Site GetSubscription] FindServerListByGroupIds error", logger.Field("error", err.Error()))
|
||||
sub.ServerCount = 0
|
||||
} else {
|
||||
sub.ServerCount = int64(len(servers))
|
||||
}
|
||||
}
|
||||
|
||||
list[i] = sub
|
||||
}
|
||||
resp.List = list
|
||||
|
||||
@ -122,6 +122,7 @@ func (l *PurchaseCheckoutLogic) PurchaseCheckout(req *types.CheckoutOrderRequest
|
||||
if err = l.balancePayment(userInfo, orderInfo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp = &types.CheckoutOrderResponse{
|
||||
Type: "balance", // Payment completed immediately
|
||||
}
|
||||
@ -325,13 +326,27 @@ func (l *PurchaseCheckoutLogic) queryExchangeRate(to string, src int64) (amount
|
||||
// balancePayment processes balance payment with gift amount priority logic
|
||||
// It prioritizes using gift amount first, then regular balance, and creates proper audit logs
|
||||
func (l *PurchaseCheckoutLogic) balancePayment(u *user.User, o *order.Order) error {
|
||||
var userInfo user.User
|
||||
var err error
|
||||
if o.Amount == 0 {
|
||||
// No payment required for zero-amount orders
|
||||
return nil
|
||||
l.Logger.Info(
|
||||
"[PurchaseCheckout] No payment required for zero-amount order",
|
||||
logger.Field("orderNo", o.OrderNo),
|
||||
logger.Field("userId", u.Id),
|
||||
)
|
||||
err := l.svcCtx.OrderModel.UpdateOrderStatus(l.ctx, o.OrderNo, 2)
|
||||
if err != nil {
|
||||
l.Errorw("[PurchaseCheckout] Update order status error",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("orderNo", o.OrderNo),
|
||||
logger.Field("userId", u.Id))
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Update order status error: %s", err.Error())
|
||||
}
|
||||
goto activation
|
||||
}
|
||||
|
||||
var userInfo user.User
|
||||
err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error {
|
||||
err = l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error {
|
||||
// Retrieve latest user information with row-level locking
|
||||
err := db.Model(&user.User{}).Where("id = ?", u.Id).First(&userInfo).Error
|
||||
if err != nil {
|
||||
@ -420,6 +435,7 @@ func (l *PurchaseCheckoutLogic) balancePayment(u *user.User, o *order.Order) err
|
||||
return err
|
||||
}
|
||||
|
||||
activation:
|
||||
// Enqueue order activation task for immediate processing
|
||||
payload := queueType.ForthwithActivateOrderPayload{
|
||||
OrderNo: o.OrderNo,
|
||||
|
||||
@ -40,6 +40,7 @@ func (l *QuerySubscribeListLogic) QuerySubscribeList() (resp *types.QuerySubscri
|
||||
list := make([]types.Subscribe, len(data))
|
||||
for i, item := range data {
|
||||
var sub types.Subscribe
|
||||
|
||||
tool.DeepCopy(&sub, item)
|
||||
if item.Discount != "" {
|
||||
var discount []types.SubscribeDiscount
|
||||
@ -48,6 +49,15 @@ func (l *QuerySubscribeListLogic) QuerySubscribeList() (resp *types.QuerySubscri
|
||||
list[i] = sub
|
||||
}
|
||||
list[i] = sub
|
||||
// 通过服务组查询关联的节点数量
|
||||
servers, err := l.svcCtx.ServerModel.FindServerListByGroupIds(l.ctx, sub.ServerGroup)
|
||||
if err != nil {
|
||||
l.Errorw("[QuerySubscribeListLogic] FindServerListByGroupIds error", logger.Field("error", err.Error()))
|
||||
sub.ServerCount = 0
|
||||
} else {
|
||||
sub.ServerCount = int64(len(servers))
|
||||
}
|
||||
list[i] = sub
|
||||
}
|
||||
resp.List = list
|
||||
return
|
||||
|
||||
@ -52,8 +52,10 @@ func (l *BindOAuthCallbackLogic) BindOAuthCallback(req *types.BindOAuthCallbackR
|
||||
err = l.google(req)
|
||||
case "apple":
|
||||
err = l.apple(req)
|
||||
case "telegram":
|
||||
err = l.telegram(req)
|
||||
default:
|
||||
l.Errorw("oauth login method not support: %v", logger.Field("method", req.Method))
|
||||
l.Errorw("oauth login method not support", logger.Field("method", req.Method))
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "oauth login method not support: %v", req.Method)
|
||||
}
|
||||
if err != nil {
|
||||
@ -212,3 +214,7 @@ func (l *BindOAuthCallbackLogic) apple(req *types.BindOAuthCallbackRequest) erro
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *BindOAuthCallbackLogic) telegram(req *types.BindOAuthCallbackRequest) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -60,6 +60,25 @@ func (l *QueryUserSubscribeLogic) QueryUserSubscribe() (resp *types.QueryUserSub
|
||||
}
|
||||
}
|
||||
|
||||
// 计算节点数量(通过服务组关联的实际节点数量)
|
||||
if item.Subscribe != nil {
|
||||
// 获取服务组ID列表
|
||||
groupIds := tool.StringToInt64Slice(item.Subscribe.ServerGroup)
|
||||
|
||||
// 通过服务组查询关联的节点数量
|
||||
servers, err := l.svcCtx.ServerModel.FindServerListByGroupIds(l.ctx, groupIds)
|
||||
if err != nil {
|
||||
l.Errorw("[QueryUserSubscribeLogic] FindServerListByGroupIds error", logger.Field("error", err.Error()))
|
||||
sub.Subscribe.ServerCount = 0
|
||||
} else {
|
||||
sub.Subscribe.ServerCount = int64(len(servers))
|
||||
}
|
||||
|
||||
// 保留原始服务器ID列表用于其他用途
|
||||
serverIds := tool.StringToInt64Slice(item.Subscribe.Server)
|
||||
sub.Subscribe.Server = serverIds
|
||||
}
|
||||
|
||||
sub.ResetTime = calculateNextResetTime(&sub)
|
||||
resp.List = append(resp.List, sub)
|
||||
}
|
||||
|
||||
@ -48,7 +48,7 @@ func (l *UpdateBindEmailLogic) UpdateBindEmail(req *types.UpdateBindEmailRequest
|
||||
if m.Id > 0 {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.UserExist), "email already bind")
|
||||
}
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
if method.Id == 0 {
|
||||
method = &user.AuthMethods{
|
||||
UserId: u.Id,
|
||||
AuthType: "email",
|
||||
|
||||
@ -9,6 +9,7 @@ import (
|
||||
"github.com/perfect-panel/server/pkg/adapter"
|
||||
"github.com/perfect-panel/server/pkg/adapter/shadowrocket"
|
||||
"github.com/perfect-panel/server/pkg/adapter/surfboard"
|
||||
"github.com/perfect-panel/server/pkg/adapter/surge"
|
||||
|
||||
"github.com/perfect-panel/server/internal/model/server"
|
||||
|
||||
@ -117,10 +118,47 @@ func (l *SubscribeLogic) getServers(userSub *user.Subscribe) ([]*server.Server,
|
||||
serverIds := tool.StringToInt64Slice(subDetails.Server)
|
||||
groupIds := tool.StringToInt64Slice(subDetails.ServerGroup)
|
||||
|
||||
// 🔍 订阅ID 2的详细调试
|
||||
if userSub.SubscribeId == 2 {
|
||||
l.Infof("🔍 [DEBUG Subscribe 2] === 开始调试订阅ID 2 ===")
|
||||
l.Infof("🔍 [DEBUG Subscribe 2] Subscribe详情: %+v", subDetails)
|
||||
l.Infof("🔍 [DEBUG Subscribe 2] Server字段: %s", subDetails.Server)
|
||||
l.Infof("🔍 [DEBUG Subscribe 2] ServerGroup字段: %s", subDetails.ServerGroup)
|
||||
l.Infof("🔍 [DEBUG Subscribe 2] 解析后的serverIds: %v", serverIds)
|
||||
l.Infof("🔍 [DEBUG Subscribe 2] 解析后的groupIds: %v", groupIds)
|
||||
}
|
||||
|
||||
l.Debugf("[Generate Subscribe]serverIds: %v, groupIds: %v", serverIds, groupIds)
|
||||
|
||||
// 查询所有服务器用于调试
|
||||
allServers, _ := l.svc.ServerModel.FindAllServer(l.ctx.Request.Context())
|
||||
if userSub.SubscribeId == 2 {
|
||||
l.Infof("🔍 [DEBUG Subscribe 2] 数据库中所有服务器:")
|
||||
for _, srv := range allServers {
|
||||
l.Infof("🔍 [DEBUG Subscribe 2] ID:%d Name:%s Protocol:%s Enable:%v GroupID:%d",
|
||||
srv.Id, srv.Name, srv.Protocol, *srv.Enable, srv.GroupId)
|
||||
}
|
||||
}
|
||||
|
||||
servers, err := l.svc.ServerModel.FindServerDetailByGroupIdsAndIds(l.ctx.Request.Context(), groupIds, serverIds)
|
||||
|
||||
if userSub.SubscribeId == 2 {
|
||||
l.Infof("🔍 [DEBUG Subscribe 2] 查询结果服务器数量: %d", len(servers))
|
||||
for i, srv := range servers {
|
||||
l.Infof("🔍 [DEBUG Subscribe 2] 结果服务器 %d: ID=%d Name=%s Protocol=%s Enable=%v",
|
||||
i+1, srv.Id, srv.Name, srv.Protocol, *srv.Enable)
|
||||
}
|
||||
|
||||
// 检查AnyTLS服务器
|
||||
anytlsServers := []*server.Server{}
|
||||
for _, srv := range servers {
|
||||
if srv.Protocol == "anytls" {
|
||||
anytlsServers = append(anytlsServers, srv)
|
||||
}
|
||||
}
|
||||
l.Infof("🔍 [DEBUG Subscribe 2] AnyTLS服务器数量: %d", len(anytlsServers))
|
||||
}
|
||||
|
||||
l.Debugf("[Query Subscribe]found servers: %v", len(servers))
|
||||
|
||||
if err != nil {
|
||||
@ -235,8 +273,9 @@ func (l *SubscribeLogic) buildClientConfig(req *types.SubscribeRequest, userSub
|
||||
})
|
||||
case "loon":
|
||||
resp = proxyManager.BuildLoon(userSub.UUID)
|
||||
l.setLoonHeaders()
|
||||
case "surfboard":
|
||||
subsURL := l.getSubscribeURL(userSub.Token)
|
||||
subsURL := l.getSubscribeURL(userSub.Token, "surfboard")
|
||||
resp = proxyManager.BuildSurfboard(l.svc.Config.Site.SiteName, surfboard.UserInfo{
|
||||
Upload: userSub.Upload,
|
||||
Download: userSub.Download,
|
||||
@ -248,7 +287,17 @@ func (l *SubscribeLogic) buildClientConfig(req *types.SubscribeRequest, userSub
|
||||
l.setSurfboardHeaders()
|
||||
case "v2rayn":
|
||||
resp = proxyManager.BuildV2rayN(userSub.UUID)
|
||||
|
||||
case "surge":
|
||||
subsURL := l.getSubscribeURL(userSub.Token, "surge")
|
||||
resp = proxyManager.BuildSurge(l.svc.Config.Site.SiteName, surge.UserInfo{
|
||||
UUID: userSub.UUID,
|
||||
Upload: userSub.Upload,
|
||||
Download: userSub.Download,
|
||||
TotalTraffic: userSub.Traffic,
|
||||
ExpiredDate: userSub.ExpireTime,
|
||||
SubscribeURL: subsURL,
|
||||
})
|
||||
l.setSurgeHeaders()
|
||||
default:
|
||||
resp = proxyManager.BuildGeneral(userSub.UUID)
|
||||
}
|
||||
@ -270,14 +319,24 @@ func (l *SubscribeLogic) setSurfboardHeaders() {
|
||||
l.ctx.Header("Content-Type", "application/octet-stream; charset=UTF-8")
|
||||
}
|
||||
|
||||
func (l *SubscribeLogic) getSubscribeURL(token string) string {
|
||||
func (l *SubscribeLogic) setSurgeHeaders() {
|
||||
l.ctx.Header("content-disposition", fmt.Sprintf("attachment;filename*=UTF-8''%s.conf", url.QueryEscape(l.svc.Config.Site.SiteName)))
|
||||
l.ctx.Header("Content-Type", "application/octet-stream; charset=UTF-8")
|
||||
}
|
||||
|
||||
func (l *SubscribeLogic) setLoonHeaders() {
|
||||
l.ctx.Header("content-disposition", fmt.Sprintf("attachment;filename*=UTF-8''%s.conf", url.QueryEscape(l.svc.Config.Site.SiteName)))
|
||||
l.ctx.Header("Content-Type", "application/octet-stream; charset=UTF-8")
|
||||
}
|
||||
|
||||
func (l *SubscribeLogic) getSubscribeURL(token, flag string) string {
|
||||
if l.svc.Config.Subscribe.PanDomain {
|
||||
return fmt.Sprintf("https://%s", l.ctx.Request.Host)
|
||||
}
|
||||
|
||||
if l.svc.Config.Subscribe.SubscribeDomain != "" {
|
||||
domains := strings.Split(l.svc.Config.Subscribe.SubscribeDomain, "\n")
|
||||
return fmt.Sprintf("https://%s%s?token=%s&flag=surfboard", domains[0], l.svc.Config.Subscribe.SubscribePath, token)
|
||||
return fmt.Sprintf("https://%s%s?token=%s&flag=%s", domains[0], l.svc.Config.Subscribe.SubscribePath, token, flag)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("https://%s%s?token=%s&flag=surfboard", l.ctx.Request.Host, l.svc.Config.Subscribe.SubscribePath, token)
|
||||
|
||||
@ -3,6 +3,8 @@ package auth
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/perfect-panel/server/pkg/email"
|
||||
)
|
||||
|
||||
type Auth struct {
|
||||
@ -124,15 +126,55 @@ type EmailAuthConfig struct {
|
||||
}
|
||||
|
||||
func (l *EmailAuthConfig) Marshal() string {
|
||||
if l.ExpirationEmailTemplate == "" {
|
||||
l.ExpirationEmailTemplate = email.DefaultExpirationEmailTemplate
|
||||
}
|
||||
if l.ExpirationEmailTemplate == "" {
|
||||
l.MaintenanceEmailTemplate = email.DefaultMaintenanceEmailTemplate
|
||||
}
|
||||
if l.TrafficExceedEmailTemplate == "" {
|
||||
l.TrafficExceedEmailTemplate = email.DefaultTrafficExceedEmailTemplate
|
||||
}
|
||||
if l.VerifyEmailTemplate == "" {
|
||||
l.VerifyEmailTemplate = email.DefaultEmailVerifyTemplate
|
||||
}
|
||||
bytes, err := json.Marshal(l)
|
||||
if err != nil {
|
||||
bytes, _ = json.Marshal(new(EmailAuthConfig))
|
||||
config := &EmailAuthConfig{
|
||||
Platform: "smtp",
|
||||
PlatformConfig: new(SMTPConfig),
|
||||
EnableVerify: true,
|
||||
EnableNotify: true,
|
||||
EnableDomainSuffix: false,
|
||||
DomainSuffixList: "",
|
||||
VerifyEmailTemplate: email.DefaultEmailVerifyTemplate,
|
||||
ExpirationEmailTemplate: email.DefaultExpirationEmailTemplate,
|
||||
MaintenanceEmailTemplate: email.DefaultMaintenanceEmailTemplate,
|
||||
TrafficExceedEmailTemplate: email.DefaultTrafficExceedEmailTemplate,
|
||||
}
|
||||
|
||||
bytes, _ = json.Marshal(config)
|
||||
}
|
||||
return string(bytes)
|
||||
}
|
||||
|
||||
func (l *EmailAuthConfig) Unmarshal(data string) error {
|
||||
return json.Unmarshal([]byte(data), &l)
|
||||
func (l *EmailAuthConfig) Unmarshal(data string) {
|
||||
err := json.Unmarshal([]byte(data), &l)
|
||||
if err != nil {
|
||||
config := &EmailAuthConfig{
|
||||
Platform: "smtp",
|
||||
PlatformConfig: new(SMTPConfig),
|
||||
EnableVerify: true,
|
||||
EnableNotify: true,
|
||||
EnableDomainSuffix: false,
|
||||
DomainSuffixList: "",
|
||||
VerifyEmailTemplate: email.DefaultEmailVerifyTemplate,
|
||||
ExpirationEmailTemplate: email.DefaultExpirationEmailTemplate,
|
||||
MaintenanceEmailTemplate: email.DefaultMaintenanceEmailTemplate,
|
||||
TrafficExceedEmailTemplate: email.DefaultTrafficExceedEmailTemplate,
|
||||
}
|
||||
_ = json.Unmarshal([]byte(config.Marshal()), &l)
|
||||
}
|
||||
}
|
||||
|
||||
// SMTPConfig Email SMTP configuration
|
||||
@ -167,13 +209,28 @@ type MobileAuthConfig struct {
|
||||
func (l *MobileAuthConfig) Marshal() string {
|
||||
bytes, err := json.Marshal(l)
|
||||
if err != nil {
|
||||
bytes, _ = json.Marshal(new(MobileAuthConfig))
|
||||
config := &MobileAuthConfig{
|
||||
Platform: "alibaba_cloud",
|
||||
PlatformConfig: new(AlibabaCloudConfig),
|
||||
EnableWhitelist: false,
|
||||
Whitelist: []string{},
|
||||
}
|
||||
bytes, _ = json.Marshal(config)
|
||||
}
|
||||
return string(bytes)
|
||||
}
|
||||
|
||||
func (l *MobileAuthConfig) Unmarshal(data string) error {
|
||||
return json.Unmarshal([]byte(data), &l)
|
||||
func (l *MobileAuthConfig) Unmarshal(data string) {
|
||||
err := json.Unmarshal([]byte(data), &l)
|
||||
if err != nil {
|
||||
config := &MobileAuthConfig{
|
||||
Platform: "alibaba_cloud",
|
||||
PlatformConfig: new(AlibabaCloudConfig),
|
||||
EnableWhitelist: false,
|
||||
Whitelist: []string{},
|
||||
}
|
||||
_ = json.Unmarshal([]byte(config.Marshal()), &l)
|
||||
}
|
||||
}
|
||||
|
||||
type AlibabaCloudConfig struct {
|
||||
|
||||
@ -32,6 +32,8 @@ type customServerLogicModel interface {
|
||||
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 (
|
||||
@ -275,3 +277,16 @@ func (m *customServerModel) FindServersByTag(ctx context.Context, tag string) ([
|
||||
})
|
||||
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))
|
||||
}
|
||||
|
||||
@ -12,6 +12,9 @@ const (
|
||||
RelayModeNone = "none"
|
||||
RelayModeAll = "all"
|
||||
RelayModeRandom = "random"
|
||||
RuleGroupTypeReject = "reject"
|
||||
RuleGroupTypeDefault = "default"
|
||||
RuleGroupTypeDirect = "direct"
|
||||
)
|
||||
|
||||
type ServerFilter struct {
|
||||
@ -178,9 +181,11 @@ 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"`
|
||||
}
|
||||
|
||||
@ -82,6 +82,7 @@ func (m *defaultUserModel) QueryUserSubscribe(ctx context.Context, userId int64,
|
||||
// 订阅过期时间大于当前时间或者订阅结束时间大于当前时间
|
||||
return conn.Where("`expire_time` > ? OR `finished_at` >= ? OR `expire_time` = ?", now, sevenDaysAgo, time.UnixMilli(0)).
|
||||
Preload("Subscribe").
|
||||
Order("created_at DESC").
|
||||
Find(&list).Error
|
||||
})
|
||||
return list, err
|
||||
|
||||
@ -32,6 +32,11 @@ type Announcement struct {
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
|
||||
type AnyTLS struct {
|
||||
Port int `json:"port" validate:"required"`
|
||||
SecurityConfig SecurityConfig `json:"security_config"`
|
||||
}
|
||||
|
||||
type AppAuthCheckRequest struct {
|
||||
Method string `json:"method" validate:"required" validate:"required,oneof=device email mobile"`
|
||||
Account string `json:"account"`
|
||||
@ -443,8 +448,10 @@ type CreatePaymentMethodRequest struct {
|
||||
type CreateRuleGroupRequest struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Icon string `json:"icon"`
|
||||
Type string `json:"type"`
|
||||
Tags []string `json:"tags"`
|
||||
Rules string `json:"rules"`
|
||||
Default bool `json:"default"`
|
||||
Enable bool `json:"enable"`
|
||||
}
|
||||
|
||||
@ -468,6 +475,7 @@ type CreateSubscribeRequest struct {
|
||||
GroupId int64 `json:"group_id"`
|
||||
ServerGroup []int64 `json:"server_group"`
|
||||
Server []int64 `json:"server"`
|
||||
ServerCount int64 `json:"server_count"`
|
||||
Show *bool `json:"show"`
|
||||
Sell *bool `json:"sell"`
|
||||
DeductionRatio int64 `json:"deduction_ratio"`
|
||||
@ -858,6 +866,7 @@ type GetStatResponse struct {
|
||||
Node int64 `json:"node"`
|
||||
Country int64 `json:"country"`
|
||||
Protocol []string `json:"protocol"`
|
||||
OnlineDevice int64 `json:"online_device"`
|
||||
}
|
||||
|
||||
type GetSubscribeDetailsRequest struct {
|
||||
@ -1038,8 +1047,9 @@ type Hysteria2 struct {
|
||||
|
||||
type InviteConfig struct {
|
||||
ForcedInvite bool `json:"forced_invite"`
|
||||
ReferralPercentage int64 `json:"referral_percentage"`
|
||||
OnlyFirstPurchase bool `json:"only_first_purchase"`
|
||||
FirstPurchasePercentage int64 `json:"first_purchase_percentage"`
|
||||
FirstYearlyPurchasePercentage int64 `json:"first_yearly_purchase_percentage"`
|
||||
NonFirstPurchasePercentage int64 `json:"non_first_purchase_percentage"`
|
||||
}
|
||||
|
||||
type KickOfflineRequest struct {
|
||||
@ -1553,9 +1563,11 @@ type ServerRuleGroup struct {
|
||||
Id int64 `json:"id"`
|
||||
Icon string `json:"icon"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Type string `json:"type"`
|
||||
Tags []string `json:"tags"`
|
||||
Rules string `json:"rules"`
|
||||
Enable bool `json:"enable"`
|
||||
Default bool `json:"default"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
@ -1647,6 +1659,7 @@ type Subscribe struct {
|
||||
GroupId int64 `json:"group_id"`
|
||||
ServerGroup []int64 `json:"server_group"`
|
||||
Server []int64 `json:"server"`
|
||||
ServerCount int64 `json:"server_count"`
|
||||
Show bool `json:"show"`
|
||||
Sell bool `json:"sell"`
|
||||
Sort int64 `json:"sort"`
|
||||
@ -1957,9 +1970,11 @@ type UpdatePaymentMethodRequest struct {
|
||||
type UpdateRuleGroupRequest struct {
|
||||
Id int64 `json:"id" validate:"required"`
|
||||
Icon string `json:"icon"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Tags []string `json:"tags"`
|
||||
Rules string `json:"rules"`
|
||||
Default bool `json:"default"`
|
||||
Enable bool `json:"enable"`
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"embed"
|
||||
|
||||
"github.com/perfect-panel/server/internal/model/server"
|
||||
"github.com/perfect-panel/server/pkg/adapter/clash"
|
||||
"github.com/perfect-panel/server/pkg/adapter/general"
|
||||
@ -10,9 +12,17 @@ import (
|
||||
"github.com/perfect-panel/server/pkg/adapter/shadowrocket"
|
||||
"github.com/perfect-panel/server/pkg/adapter/singbox"
|
||||
"github.com/perfect-panel/server/pkg/adapter/surfboard"
|
||||
"github.com/perfect-panel/server/pkg/adapter/surge"
|
||||
"github.com/perfect-panel/server/pkg/adapter/v2rayn"
|
||||
)
|
||||
|
||||
//go:embed template/*
|
||||
var TemplateFS embed.FS
|
||||
|
||||
var (
|
||||
AutoSelect = "Auto - UrlTest"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Nodes []*server.Server
|
||||
Rules []*server.RuleGroup
|
||||
@ -25,62 +35,68 @@ type Adapter struct {
|
||||
|
||||
func NewAdapter(cfg *Config) *Adapter {
|
||||
// 转换服务器列表
|
||||
proxies := adapterProxies(cfg.Nodes)
|
||||
// 生成代理组
|
||||
proxyGroup, region := generateProxyGroup(proxies)
|
||||
|
||||
proxies, nodes, tags := adapterProxies(cfg.Nodes)
|
||||
// 转换规则组
|
||||
g, r := adapterRules(cfg.Rules)
|
||||
|
||||
// 加入兜底节点
|
||||
for i, group := range g {
|
||||
if len(group.Proxies) == 0 {
|
||||
g[i].Proxies = append([]string{"DIRECT"}, region...)
|
||||
g, r, d := adapterRules(cfg.Rules)
|
||||
if d == "" {
|
||||
d = AutoSelect
|
||||
}
|
||||
}
|
||||
|
||||
// 生成默认代理组
|
||||
proxyGroup := append(generateDefaultGroup(), g...)
|
||||
// 合并代理组
|
||||
proxyGroup = RemoveEmptyGroup(append(proxyGroup, g...))
|
||||
// 处理标签
|
||||
proxyGroup = adapterTags(cfg.Tags, proxyGroup)
|
||||
proxyGroup = SortGroups(proxyGroup, nodes, tags, d)
|
||||
return &Adapter{
|
||||
Adapter: proxy.Adapter{
|
||||
Proxies: proxies,
|
||||
Group: proxyGroup,
|
||||
Rules: r,
|
||||
Region: region,
|
||||
Nodes: nodes,
|
||||
Default: d,
|
||||
TemplateFS: &TemplateFS,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// BuildClash generates a Clash configuration for the given UUID.
|
||||
func (m *Adapter) BuildClash(uuid string) ([]byte, error) {
|
||||
client := clash.NewClash(m.Adapter)
|
||||
return client.Build(uuid)
|
||||
}
|
||||
|
||||
// BuildGeneral generates a general configuration for the given UUID.
|
||||
func (m *Adapter) BuildGeneral(uuid string) []byte {
|
||||
return general.GenerateBase64General(m.Proxies, uuid)
|
||||
}
|
||||
|
||||
// BuildLoon generates a Loon configuration for the given UUID.
|
||||
func (m *Adapter) BuildLoon(uuid string) []byte {
|
||||
return loon.BuildLoon(m.Proxies, uuid)
|
||||
}
|
||||
|
||||
// BuildQuantumultX generates a Quantumult X configuration for the given UUID.
|
||||
func (m *Adapter) BuildQuantumultX(uuid string) string {
|
||||
return quantumultx.BuildQuantumultX(m.Proxies, uuid)
|
||||
}
|
||||
|
||||
// BuildSingbox generates a Singbox configuration for the given UUID.
|
||||
func (m *Adapter) BuildSingbox(uuid string) ([]byte, error) {
|
||||
return singbox.BuildSingbox(m.Adapter, uuid)
|
||||
}
|
||||
|
||||
func (m *Adapter) BuildShadowrocket(uuid string, userInfo shadowrocket.UserInfo) []byte {
|
||||
return shadowrocket.BuildShadowrocket(m.Proxies, uuid, userInfo)
|
||||
}
|
||||
|
||||
// BuildSurfboard generates a Surfboard configuration for the given site name and user info.
|
||||
func (m *Adapter) BuildSurfboard(siteName string, user surfboard.UserInfo) []byte {
|
||||
return surfboard.BuildSurfboard(m.Adapter, siteName, user)
|
||||
}
|
||||
|
||||
// BuildV2rayN generates a V2rayN configuration for the given UUID.
|
||||
func (m *Adapter) BuildV2rayN(uuid string) []byte {
|
||||
return v2rayn.NewV2rayN(m.Adapter).Build(uuid)
|
||||
}
|
||||
|
||||
// BuildSurge generates a Surge configuration for the given UUID and site name.
|
||||
func (m *Adapter) BuildSurge(siteName string, user surge.UserInfo) []byte {
|
||||
return surge.NewSurge(m.Adapter).Build(siteName, user)
|
||||
}
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
package clash
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"text/template"
|
||||
|
||||
"github.com/Masterminds/sprig/v3"
|
||||
"github.com/perfect-panel/server/pkg/adapter/proxy"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
"gopkg.in/yaml.v3"
|
||||
@ -20,22 +23,16 @@ func NewClash(adapter proxy.Adapter) *Clash {
|
||||
|
||||
func (c *Clash) Build(uuid string) ([]byte, error) {
|
||||
var proxies []Proxy
|
||||
for _, v := range c.Proxies {
|
||||
p, err := c.parseProxy(v, uuid)
|
||||
for _, proxied := range c.Adapter.Proxies {
|
||||
p, err := c.parseProxy(proxied, uuid)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to parse proxy for %s: %s", v.Name, err.Error())
|
||||
logger.Errorw("Failed to parse proxy", logger.Field("error", err), logger.Field("proxy", p.Name))
|
||||
continue
|
||||
}
|
||||
proxies = append(proxies, *p)
|
||||
}
|
||||
var rawConfig RawConfig
|
||||
if err := yaml.Unmarshal([]byte(DefaultTemplate), &rawConfig); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal template: %w", err)
|
||||
}
|
||||
rawConfig.Proxies = proxies
|
||||
// generate proxy groups
|
||||
var groups []ProxyGroup
|
||||
for _, group := range c.Group {
|
||||
for _, group := range c.Adapter.Group {
|
||||
groups = append(groups, ProxyGroup{
|
||||
Name: group.Name,
|
||||
Type: string(group.Type),
|
||||
@ -44,9 +41,38 @@ func (c *Clash) Build(uuid string) ([]byte, error) {
|
||||
Interval: group.Interval,
|
||||
})
|
||||
}
|
||||
rawConfig.ProxyGroups = groups
|
||||
rawConfig.Rules = append(c.Rules, "MATCH,手动选择")
|
||||
return yaml.Marshal(&rawConfig)
|
||||
var rules = append(c.Rules, fmt.Sprintf("MATCH,%s", c.Default))
|
||||
|
||||
tmplBytes, err := c.TemplateFS.ReadFile("template/clash.tpl")
|
||||
if err != nil {
|
||||
logger.Errorw("Failed to read template file", logger.Field("error", err))
|
||||
return nil, fmt.Errorf("failed to read template file: %w", err)
|
||||
}
|
||||
tpl, err := template.New("clash.yaml").Funcs(sprig.FuncMap()).Funcs(template.FuncMap{
|
||||
"toYaml": func(v interface{}) string {
|
||||
out, err := yaml.Marshal(v)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("# YAML encode error: %v", err.Error())
|
||||
}
|
||||
return string(out)
|
||||
},
|
||||
}).Parse(string(tmplBytes))
|
||||
if err != nil {
|
||||
logger.Errorw("[Clash] Failed to parse template", logger.Field("error", err))
|
||||
return nil, fmt.Errorf("failed to parse template: %w", err)
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
err = tpl.Execute(&buf, map[string]interface{}{
|
||||
"Proxies": proxies,
|
||||
"ProxyGroups": groups,
|
||||
"Rules": rules,
|
||||
})
|
||||
if err != nil {
|
||||
logger.Errorw("[Clash] Failed to execute template", logger.Field("error", err))
|
||||
return nil, fmt.Errorf("failed to execute template: %w", err)
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (c *Clash) parseProxy(p proxy.Proxy, uuid string) (*Proxy, error) {
|
||||
|
||||
@ -1,31 +1,50 @@
|
||||
package clash
|
||||
|
||||
const DefaultTemplate = `
|
||||
mixed-port: 7890
|
||||
mode: rule
|
||||
ipv6: true
|
||||
allow-lan: true
|
||||
bind-address: "*"
|
||||
mode: rule
|
||||
log-level: info
|
||||
external-controller: 127.0.0.1:9090
|
||||
global-client-fingerprint: chrome
|
||||
mixed-port: 7890
|
||||
log-level: error
|
||||
unified-delay: true
|
||||
geox-url:
|
||||
mmdb: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.metadb"
|
||||
tcp-concurrent: true
|
||||
external-controller: 0.0.0.0:9090
|
||||
|
||||
tun:
|
||||
enable: true
|
||||
stack: system
|
||||
auto-route: true
|
||||
|
||||
dns:
|
||||
enable: true
|
||||
cache-algorithm: arc
|
||||
listen: 0.0.0.0:1053
|
||||
ipv6: true
|
||||
enhanced-mode: fake-ip
|
||||
fake-ip-range: 198.18.0.1/16
|
||||
use-hosts: true
|
||||
fake-ip-filter:
|
||||
- "*.lan"
|
||||
- "lens.l.google.com"
|
||||
- "*.srv.nintendo.net"
|
||||
- "*.stun.playstation.net"
|
||||
- "xbox.*.*.microsoft.com"
|
||||
- "*.xboxlive.com"
|
||||
- "*.msftncsi.com"
|
||||
- "*.msftconnecttest.com"
|
||||
default-nameserver:
|
||||
- 120.53.53.53
|
||||
- 1.12.12.12
|
||||
- 119.29.29.29
|
||||
- 223.5.5.5
|
||||
nameserver:
|
||||
- https://120.53.53.53/dns-query#skip-cert-verify=true
|
||||
- tls://1.12.12.12#skip-cert-verify=true
|
||||
proxy-server-nameserver:
|
||||
- https://120.53.53.53/dns-query#skip-cert-verify=true
|
||||
- tls://1.12.12.12#skip-cert-verify=true
|
||||
- system
|
||||
- 119.29.29.29
|
||||
- 223.5.5.5
|
||||
fallback:
|
||||
- 8.8.8.8
|
||||
- 1.1.1.1
|
||||
fallback-filter:
|
||||
geoip: true
|
||||
geoip-code: CN
|
||||
|
||||
proxies:
|
||||
|
||||
|
||||
@ -63,6 +63,8 @@ func buildProxy(data proxy.Proxy, uuid string) string {
|
||||
return Hysteria2Uri(data, uuid)
|
||||
case "tuic":
|
||||
return TuicUri(data, uuid)
|
||||
case "anytls":
|
||||
return AnyTLSUri(data, uuid)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
@ -271,6 +273,36 @@ func TuicUri(data proxy.Proxy, uuid string) string {
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func AnyTLSUri(data proxy.Proxy, uuid string) string {
|
||||
anytls, ok := data.Option.(proxy.AnyTLS)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
securityConfig := anytls.SecurityConfig
|
||||
var query = make(url.Values)
|
||||
|
||||
// 根据AnyTLS官方URI规范实现
|
||||
// 格式: anytls://[auth@]hostname[:port]/?[key=value]&[key=value]...
|
||||
|
||||
// TLS配置
|
||||
setQuery(&query, "sni", securityConfig.SNI)
|
||||
|
||||
// 是否允许不安全连接
|
||||
if securityConfig.AllowInsecure {
|
||||
setQuery(&query, "insecure", "1")
|
||||
}
|
||||
|
||||
u := url.URL{
|
||||
Scheme: "anytls",
|
||||
User: url.User(uuid),
|
||||
Host: net.JoinHostPort(data.Server, strconv.Itoa(anytls.Port)),
|
||||
RawQuery: query.Encode(),
|
||||
Fragment: data.Name,
|
||||
}
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func setQuery(q *url.Values, k, v string) {
|
||||
if v != "" {
|
||||
q.Set(k, v)
|
||||
|
||||
@ -1,27 +1,61 @@
|
||||
package loon
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/perfect-panel/server/pkg/adapter/proxy"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
)
|
||||
|
||||
//go:embed *.tpl
|
||||
var configFiles embed.FS
|
||||
|
||||
func BuildLoon(servers []proxy.Proxy, uuid string) []byte {
|
||||
uri := ""
|
||||
nodes := make([]string, 0)
|
||||
for _, s := range servers {
|
||||
switch s.Protocol {
|
||||
case "vmess":
|
||||
nodes = append(nodes, s.Name)
|
||||
uri += buildVMess(s, uuid)
|
||||
case "shadowsocks":
|
||||
nodes = append(nodes, s.Name)
|
||||
uri += buildShadowsocks(s, uuid)
|
||||
case "trojan":
|
||||
nodes = append(nodes, s.Name)
|
||||
uri += buildTrojan(s, uuid)
|
||||
case "vless":
|
||||
nodes = append(nodes, s.Name)
|
||||
uri += buildVless(s, uuid)
|
||||
case "hysteria2":
|
||||
nodes = append(nodes, s.Name)
|
||||
uri += buildHysteria2(s, uuid)
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return []byte(uri)
|
||||
file, err := configFiles.ReadFile("default.tpl")
|
||||
if err != nil {
|
||||
logger.Errorf("read default surfboard config error: %v", err.Error())
|
||||
return nil
|
||||
}
|
||||
// replace template
|
||||
tpl, err := template.New("default").Parse(string(file))
|
||||
if err != nil {
|
||||
logger.Errorf("read default surfboard config error: %v", err.Error())
|
||||
return nil
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err = tpl.Execute(&buf, map[string]interface{}{
|
||||
"Proxies": uri,
|
||||
"Nodes": strings.Join(nodes, ","),
|
||||
}); err != nil {
|
||||
logger.Errorf("Execute Loon template error: %v", err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
58
pkg/adapter/loon/default.tpl
Normal file
58
pkg/adapter/loon/default.tpl
Normal file
@ -0,0 +1,58 @@
|
||||
[General]
|
||||
ipv6-vif = auto
|
||||
ip-mode = dual
|
||||
skip-proxy = 192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,127.0.0.0/8,localhost,*.local
|
||||
bypass-tun = 192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,127.0.0.0/8,localhost,*.local
|
||||
dns-server = system,119.29.29.29,223.5.5.5
|
||||
hijack-dns = 8.8.8.8:53,8.8.4.4:53,1.1.1.1:53,1.0.0.1:53
|
||||
allow-wifi-access = true
|
||||
wifi-access-http-port = 6888
|
||||
wifi-access-socks5-port = 6889
|
||||
proxy-test-url = http://bing.com/generate_204
|
||||
internet-test-url = http://wifi.vivo.com.cn/generate_204
|
||||
test-timeout = 5
|
||||
interface-mode = auto
|
||||
|
||||
[Proxy]
|
||||
{{.Proxies}}
|
||||
|
||||
[Proxy Group]
|
||||
🚀 Proxy = select,🌏 Auto,{{.Nodes}}
|
||||
🌏 Auto = fallback,{{.Nodes}},interval = 600,max-timeout = 3000
|
||||
🍎 Apple = select,🚀 Proxy,🎯 Direct,{{.Nodes}}
|
||||
🔍 Google = select,🚀 Proxy,{{.Nodes}}
|
||||
🪟 Microsoft = select,🚀 Proxy,🎯 Direct,{{.Nodes}}
|
||||
📠 X = select,🚀 Proxy,{{.Nodes}}
|
||||
🤖 AI = select,🚀 Proxy,🎯 Direct,{{.Nodes}}
|
||||
📟 Telegram = select,🚀 Proxy,{{.Nodes}}
|
||||
📺 YouTube = select,🚀 Proxy,{{.Nodes}}
|
||||
🇨🇳 China = select,🎯 Direct,🚀 Proxy,{{.Nodes}}
|
||||
🐠 Final = select,🚀 Proxy,🎯 Direct,{{.Nodes}}
|
||||
🎯 Direct = select,DIRECT
|
||||
|
||||
[Remote Rule]
|
||||
https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/rule/Loon/Apple/Apple.list, policy=🍎 Apple, tag=Apple, enabled=true
|
||||
https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/rule/Loon/Apple/Apple_Domain.list, policy=🍎 Apple, tag=Apple_Domain, enabled=true
|
||||
https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/rule/Loon/Google/Google.list, policy=🔍 Google, tag=Google, enabled=true
|
||||
https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/rule/Loon/Microsoft/Microsoft.list, policy=🪟 Microsoft, tag=Microsoft, enabled=true
|
||||
https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/rule/Loon/Twitter/Twitter.list, policy=📠 X, tag=X, enabled=true
|
||||
https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/rule/Loon/OpenAI/OpenAI.list, policy=🤖 AI, tag=OpenAI, enabled=true
|
||||
https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/rule/Loon/Telegram/Telegram.list, policy=📟 Telegram, tag=Telegram, enabled=true
|
||||
https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/rule/Loon/YouTube/YouTube.list, policy=📺 YouTube, tag=YouTube, enabled=true
|
||||
https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/rule/Loon/YouTubeMusic/YouTubeMusic.list, policy=📺 YouTube, tag=YouTubeMusic, enabled=true
|
||||
https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/rule/Loon/Global/Global.list, policy=🚀 Proxy, tag=Global, enabled=true
|
||||
https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/rule/Loon/Global/Global_Domain.list, policy=🚀 Proxy, tag=Global_Domain, enabled=true
|
||||
https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/rule/Loon/ChinaMax/ChinaMax.list, policy=🇨🇳 China, tag=ChinaMax, enabled=true
|
||||
https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/rule/Loon/ChinaMax/ChinaMax_Domain.list, policy=🇨🇳 China, tag=ChinaMax_Domain, enabled=true
|
||||
https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/rule/Loon/Lan/Lan.list, policy=🎯 Direct, tag=LAN, enabled=true
|
||||
|
||||
[Rule]
|
||||
GEOIP,CN,🇨🇳 China
|
||||
FINAL,🐠 Final
|
||||
|
||||
[Rewrite]
|
||||
# Redirect Google Service
|
||||
^https?:\/\/(www.)?g\.cn 302 https://www.google.com
|
||||
^https?:\/\/(www.)?google\.cn 302 https://www.google.com
|
||||
# Redirect Githubusercontent
|
||||
^https://.*\.githubusercontent\.com\/ header-replace Accept-Language en-us
|
||||
@ -1,21 +1,26 @@
|
||||
package proxy
|
||||
|
||||
import "embed"
|
||||
|
||||
// Adapter represents a proxy adapter
|
||||
type Adapter struct {
|
||||
Proxies []Proxy
|
||||
Group []Group
|
||||
Rules []string
|
||||
Region []string
|
||||
Rules []string // rule
|
||||
Nodes []string // all node
|
||||
Default string // Default Node
|
||||
TemplateFS *embed.FS // Template file system
|
||||
}
|
||||
|
||||
// Proxy represents a proxy server
|
||||
type Proxy struct {
|
||||
Name string
|
||||
Server string
|
||||
Port int
|
||||
Protocol string
|
||||
Country string
|
||||
Option any
|
||||
Name string // Name of the proxy
|
||||
Server string // Server address of the proxy
|
||||
Port int // Port of the proxy server
|
||||
Protocol string // Protocol type (e.g., shadowsocks, vless, vmess, trojan, hysteria2, tuic, anytls)
|
||||
Country string // Country of the proxy
|
||||
Tags []string // Tags for the proxy
|
||||
Option any // Additional options for the proxy configuration
|
||||
}
|
||||
|
||||
// Group represents a group of proxies
|
||||
@ -25,6 +30,10 @@ type Group struct {
|
||||
Proxies []string
|
||||
URL string
|
||||
Interval int
|
||||
Reject bool // Reject group
|
||||
Direct bool // Direct group
|
||||
Tags []string // Tags for the group
|
||||
Default bool // Default group
|
||||
}
|
||||
|
||||
type GroupType string
|
||||
|
||||
@ -79,7 +79,7 @@ func BuildSingbox(adapter proxy.Adapter, uuid string) ([]byte, error) {
|
||||
|
||||
rawConfig["outbounds"] = proxies
|
||||
route := RouteOptions{
|
||||
Final: "手动选择",
|
||||
Final: adapter.Default,
|
||||
Rules: []Rule{
|
||||
{
|
||||
Inbound: []string{
|
||||
@ -114,7 +114,7 @@ func BuildSingbox(adapter proxy.Adapter, uuid string) ([]byte, error) {
|
||||
},
|
||||
{
|
||||
ClashMode: "global",
|
||||
Outbound: "手动选择",
|
||||
Outbound: adapter.Default,
|
||||
},
|
||||
{
|
||||
IPIsPrivate: true,
|
||||
|
||||
@ -4,7 +4,6 @@ import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
@ -23,41 +22,21 @@ var shadowsocksSupportMethod = []string{"aes-128-gcm", "aes-192-gcm", "aes-256-g
|
||||
func BuildSurfboard(servers proxy.Adapter, siteName string, user UserInfo) []byte {
|
||||
var proxies, proxyGroup string
|
||||
var removed []string
|
||||
for _, node := range servers.Proxies {
|
||||
if uri := buildProxy(node, user.UUID); uri != "" {
|
||||
proxies += uri
|
||||
} else {
|
||||
removed = append(removed, node.Name)
|
||||
}
|
||||
}
|
||||
var ps []string
|
||||
|
||||
for _, group := range servers.Group {
|
||||
|
||||
if len(removed) > 0 {
|
||||
group.Proxies = tool.RemoveStringElement(group.Proxies, removed...)
|
||||
for _, p := range servers.Proxies {
|
||||
switch p.Protocol {
|
||||
case "shadowsocks":
|
||||
proxies += buildShadowsocks(p, user.UUID)
|
||||
case "trojan":
|
||||
proxies += buildTrojan(p, user.UUID)
|
||||
case "vmess":
|
||||
proxies += buildVMess(p, user.UUID)
|
||||
default:
|
||||
removed = append(removed, p.Name)
|
||||
}
|
||||
|
||||
if group.Type == proxy.GroupTypeSelect {
|
||||
proxyGroup += fmt.Sprintf("%s = select, %s", group.Name, strings.Join(group.Proxies, ", ")) + "\r\n"
|
||||
} else if group.Type == proxy.GroupTypeURLTest {
|
||||
proxyGroup += fmt.Sprintf("%s = url-test, %s, url=%s, interval=%d", group.Name, strings.Join(group.Proxies, ", "), group.URL, group.Interval) + "\r\n"
|
||||
} else if group.Type == proxy.GroupTypeFallback {
|
||||
proxyGroup += fmt.Sprintf("%s = fallback, %s, url=%s, interval=%d", group.Name, strings.Join(group.Proxies, ", "), group.URL, group.Interval) + "\r\n"
|
||||
} else {
|
||||
logger.Errorf("[BuildSurfboard] unknown group type: %s", group.Type)
|
||||
ps = append(ps, p.Name)
|
||||
}
|
||||
}
|
||||
|
||||
var rules string
|
||||
for _, rule := range servers.Rules {
|
||||
if rule == "" {
|
||||
continue
|
||||
}
|
||||
rules += rule + "\r\n"
|
||||
}
|
||||
|
||||
//final rule
|
||||
rules += "FINAL, 手动选择"
|
||||
|
||||
file, err := configFiles.ReadFile("default.tpl")
|
||||
if err != nil {
|
||||
@ -78,42 +57,24 @@ func BuildSurfboard(servers proxy.Adapter, siteName string, user UserInfo) []byt
|
||||
} else {
|
||||
expiredAt = user.ExpiredDate.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
ps = tool.RemoveStringElement(ps, removed...)
|
||||
proxyGroup = strings.Join(ps, ",")
|
||||
|
||||
// convert traffic
|
||||
upload := traffic.AutoConvert(user.Upload, false)
|
||||
download := traffic.AutoConvert(user.Download, false)
|
||||
total := traffic.AutoConvert(user.TotalTraffic, false)
|
||||
unusedTraffic := traffic.AutoConvert(user.TotalTraffic-user.Upload-user.Download, false)
|
||||
// query Host
|
||||
urlParse, err := url.Parse(user.SubscribeURL)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if err := tpl.Execute(&buf, map[string]interface{}{
|
||||
if err = tpl.Execute(&buf, map[string]interface{}{
|
||||
"Proxies": proxies,
|
||||
"ProxyGroup": proxyGroup,
|
||||
"SubscribeURL": user.SubscribeURL,
|
||||
"SubscribeInfo": fmt.Sprintf("title=%s订阅信息, content=上传流量:%s\\n下载流量:%s\\n剩余流量: %s\\n套餐流量:%s\\n到期时间:%s", siteName, upload, download, unusedTraffic, total, expiredAt),
|
||||
"SubscribeDomain": urlParse.Host,
|
||||
"Rules": rules,
|
||||
}); err != nil {
|
||||
logger.Errorf("build surfboard config error: %v", err.Error())
|
||||
logger.Errorf("build Surge config error: %v", err.Error())
|
||||
return nil
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func buildProxy(data proxy.Proxy, uuid string) string {
|
||||
var p string
|
||||
switch data.Protocol {
|
||||
case "vmess":
|
||||
p = buildVMess(data, uuid)
|
||||
case "shadowsocks":
|
||||
if !tool.Contains(shadowsocksSupportMethod, data.Option.(proxy.Shadowsocks).Method) {
|
||||
return ""
|
||||
}
|
||||
p = buildShadowsocks(data, uuid)
|
||||
case "trojan":
|
||||
p = buildTrojan(data, uuid)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
@ -1,29 +1,62 @@
|
||||
#!MANAGED-CONFIG {{.SubscribeURL}} interval=43200 strict=true
|
||||
|
||||
[General]
|
||||
loglevel = notify
|
||||
ipv6 = false
|
||||
skip-proxy = localhost, *.local, injections.adguard.org, local.adguard.org, 0.0.0.0/8, 10.0.0.0/8, 17.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.0.0.0/24, 192.0.2.0/24, 192.168.0.0/16, 192.88.99.0/24, 198.18.0.0/15, 198.51.100.0/24, 203.0.113.0/24, 224.0.0.0/4, 240.0.0.0/4, 255.255.255.255/32
|
||||
tls-provider = default
|
||||
show-error-page-for-reject = true
|
||||
dns-server = 223.6.6.6, 119.29.29.29, 119.28.28.28
|
||||
dns-server = system, 119.29.29.29, 223.5.5.5
|
||||
skip-proxy = 192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12, 127.0.0.0/8, localhost, *.local
|
||||
always-real-ip = *.lan, lens.l.google.com, *.srv.nintendo.net, *.stun.playstation.net, *.xboxlive.com, xbox.*.*.microsoft.com, *.msftncsi.com, *.msftconnecttest.com
|
||||
proxy-test-url = http://www.gstatic.com/generate_204
|
||||
internet-test-url = http://connectivitycheck.platform.hicloud.com/generate_204
|
||||
test-timeout = 5
|
||||
internet-test-url = http://bing.com
|
||||
proxy-test-url = http://bing.com
|
||||
http-listen = 0.0.0.0:6088
|
||||
socks5-listen = 0.0.0.0:6089
|
||||
|
||||
[Panel]
|
||||
SubscribeInfo = {{.SubscribeInfo}}, style=info
|
||||
|
||||
# Surfboard 配置文档:https://manual.getsurfboard.com/
|
||||
|
||||
[Proxy]
|
||||
# 代理列表
|
||||
{{.Proxies}}
|
||||
|
||||
[Proxy Group]
|
||||
# 代理组列表
|
||||
{{ .ProxyGroup }}
|
||||
🚀 Proxy = select, 🌏 Auto, 🎯 Direct, include-other-group=🇺🇳 Nodes
|
||||
🍎 Apple = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes
|
||||
🔍 Google = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes
|
||||
🪟 Microsoft = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes
|
||||
📺 GlobalMedia = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes
|
||||
🤖 AI = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes
|
||||
🪙 Crypto = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes
|
||||
🎮 Game = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes
|
||||
📟 Telegram = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes
|
||||
🇨🇳 China = select, 🎯 Direct, 🚀 Proxy, include-other-group=🇺🇳 Nodes
|
||||
🐠 Final = select, 🎯 Direct, 🚀 Proxy, include-other-group=🇺🇳 Nodes
|
||||
🌏 Auto = fallback, include-other-group=🇺🇳 Nodes, url=http://www.gstatic.com/generate_204, interval=600, timeout=5
|
||||
🎯 Direct = select, DIRECT, hidden=1
|
||||
🇺🇳 Nodes = select, {{.ProxyGroup}}, hidden=1
|
||||
|
||||
[Rule]
|
||||
# 规则列表
|
||||
{{ .Rules }}
|
||||
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Apple/Apple_All.list, 🍎 Apple
|
||||
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Google/Google.list, 🔍 Google
|
||||
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/GitHub/GitHub.list, 🪟 Microsoft
|
||||
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Microsoft/Microsoft.list, 🪟 Microsoft
|
||||
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/HBO/HBO.list, 📺 GlobalMedia
|
||||
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Disney/Disney.list, 📺 GlobalMedia
|
||||
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/TikTok/TikTok.list, 📺 GlobalMedia
|
||||
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Netflix/Netflix.list, 📺 GlobalMedia
|
||||
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/GlobalMedia/GlobalMedia_All_No_Resolve.list, 📺 GlobalMedia
|
||||
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Telegram/Telegram.list, 📟 Telegram
|
||||
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/OpenAI/OpenAI.list, 🤖 AI
|
||||
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Gemini/Gemini.list, 🤖 AI
|
||||
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Copilot/Copilot.list, 🤖 AI
|
||||
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Claude/Claude.list, 🤖 AI
|
||||
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Crypto/Crypto.list, 🪙 Crypto
|
||||
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Cryptocurrency/Cryptocurrency.list, 🪙 Crypto
|
||||
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Game/Game.list, 🎮 Game
|
||||
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Global/Global_All_No_Resolve.list, 🚀 Proxy
|
||||
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/ChinaMax/ChinaMax_All_No_Resolve.list, 🇨🇳 China
|
||||
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Lan/Lan.list, 🎯 Direct
|
||||
|
||||
GEOIP, CN, 🇨🇳 China
|
||||
FINAL, 🐠 Final, dns-failed
|
||||
|
||||
[URL Rewrite]
|
||||
^https?:\/\/(www.)?g\.cn https://www.google.com 302
|
||||
^https?:\/\/(www.)?google\.cn https://www.google.com 302
|
||||
@ -1,61 +1,79 @@
|
||||
#!MANAGED-CONFIG {{.SubscribeURL}} interval=43200 strict=true
|
||||
# Surge 的规则配置手册: https://manual.nssurge.com/
|
||||
|
||||
[General]
|
||||
loglevel = notify
|
||||
# 从 Surge iOS 4 / Surge Mac 3.3.0 起,工具开始支持 DoH
|
||||
doh-server = https://doh.pub/dns-query
|
||||
# https://dns.alidns.com/dns-query, https://13800000000.rubyfish.cn/, https://dns.google/dns-query
|
||||
dns-server = 223.5.5.5, 114.114.114.114
|
||||
tun-excluded-routes = 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.0.0.0/24, 192.0.2.0/24, 192.168.0.0/16, 192.88.99.0/24, 198.51.100.0/24, 203.0.113.0/24, 224.0.0.0/4, 255.255.255.255/32
|
||||
skip-proxy = localhost, *.local, injections.adguard.org, local.adguard.org, captive.apple.com, guzzoni.apple.com, 0.0.0.0/8, 10.0.0.0/8, 17.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.0.0.0/24, 192.0.2.0/24, 192.168.0.0/16, 192.88.99.0/24, 198.18.0.0/15, 198.51.100.0/24, 203.0.113.0/24, 224.0.0.0/4, 240.0.0.0/4, 255.255.255.255/32
|
||||
|
||||
wifi-assist = true
|
||||
allow-wifi-access = true
|
||||
wifi-access-http-port = 6152
|
||||
wifi-access-socks5-port = 6153
|
||||
http-listen = 0.0.0.0:6152
|
||||
socks5-listen = 0.0.0.0:6153
|
||||
|
||||
external-controller-access = surgepasswd@0.0.0.0:6170
|
||||
replica = false
|
||||
|
||||
tls-provider = openssl
|
||||
network-framework = false
|
||||
external-controller-access = purinio@0.0.0.0:6170
|
||||
exclude-simple-hostnames = true
|
||||
show-error-page-for-reject = true
|
||||
udp-priority = true
|
||||
udp-policy-not-supported-behaviour = reject
|
||||
ipv6 = true
|
||||
|
||||
test-timeout = 4
|
||||
ipv6-vif = auto
|
||||
proxy-test-url = http://www.gstatic.com/generate_204
|
||||
geoip-maxmind-url = https://unpkg.zhimg.com/rulestatic@1.0.1/Country.mmdb
|
||||
internet-test-url = http://connectivitycheck.platform.hicloud.com/generate_204
|
||||
test-timeout = 5
|
||||
dns-server = system, 119.29.29.29, 223.5.5.5
|
||||
hijack-dns = 8.8.8.8:53, 8.8.4.4:53, 1.1.1.1:53, 1.0.0.1:53
|
||||
skip-proxy = 192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12, 127.0.0.0/8, localhost, *.local
|
||||
always-real-ip = *.lan, lens.l.google.com, *.srv.nintendo.net, *.stun.playstation.net, *.xboxlive.com, xbox.*.*.microsoft.com, *.msftncsi.com, *.msftconnecttest.com
|
||||
|
||||
[Replica]
|
||||
hide-apple-request = true
|
||||
hide-crashlytics-request = true
|
||||
use-keyword-filter = false
|
||||
hide-udp = false
|
||||
# > Surge Mac Parameters
|
||||
http-listen = 0.0.0.0:6088
|
||||
socks5-listen = 0.0.0.0:6089
|
||||
|
||||
# > Surge iOS Parameters
|
||||
allow-wifi-access = true
|
||||
allow-hotspot-access = true
|
||||
wifi-access-http-port = 6088
|
||||
wifi-access-socks5-port = 6089
|
||||
|
||||
[Panel]
|
||||
SubscribeInfo = {{.SubscribeInfo}}, style=info
|
||||
|
||||
# -----------------------------
|
||||
# Surge 的几种策略配置规范,请参考 https://manual.nssurge.com/policy/proxy.html
|
||||
# 不同的代理策略有*很多*可选参数,请参考上方连接的 Parameters 一段,根据需求自行添加参数。
|
||||
#
|
||||
# Surge 现已支持 UDP 转发功能,请参考: https://trello.com/c/ugOMxD3u/53-udp-%E8%BD%AC%E5%8F%91
|
||||
# Surge 现已支持 TCP-Fast-Open 技术,请参考: https://trello.com/c/ij65BU6Q/48-tcp-fast-open-troubleshooting-guide
|
||||
# Surge 现已支持 ss-libev 的全部加密方式和混淆,请参考: https://trello.com/c/BTr0vG1O/47-ss-libev-%E7%9A%84%E6%94%AF%E6%8C%81%E6%83%85%E5%86%B5
|
||||
# -----------------------------
|
||||
|
||||
[Proxy]
|
||||
{{.Proxies}}
|
||||
|
||||
[Proxy Group]
|
||||
# 代理组列表
|
||||
{{ .ProxyGroup }}
|
||||
🚀 Proxy = select, 🌏 Auto, 🎯 Direct, include-other-group=🇺🇳 Nodes
|
||||
🍎 Apple = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes
|
||||
🔍 Google = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes
|
||||
🪟 Microsoft = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes
|
||||
📺 GlobalMedia = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes
|
||||
🤖 AI = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes
|
||||
🪙 Crypto = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes
|
||||
🎮 Game = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes
|
||||
📟 Telegram = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes
|
||||
🇨🇳 China = select, 🎯 Direct, 🚀 Proxy, include-other-group=🇺🇳 Nodes
|
||||
🐠 Final = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes
|
||||
🌏 Auto = smart, include-other-group=🇺🇳 Nodes
|
||||
🎯 Direct = select, DIRECT, hidden=1
|
||||
🇺🇳 Nodes = select, {{.ProxyGroup}}, hidden=1
|
||||
|
||||
[Rule]
|
||||
{{ .Rules }}
|
||||
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Apple/Apple_All.list, 🍎 Apple
|
||||
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Google/Google.list, 🔍 Google
|
||||
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/GitHub/GitHub.list, 🪟 Microsoft
|
||||
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Microsoft/Microsoft.list, 🪟 Microsoft
|
||||
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/HBO/HBO.list, 📺 GlobalMedia
|
||||
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Disney/Disney.list, 📺 GlobalMedia
|
||||
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/TikTok/TikTok.list, 📺 GlobalMedia
|
||||
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Netflix/Netflix.list, 📺 GlobalMedia
|
||||
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/GlobalMedia/GlobalMedia_All_No_Resolve.list, 📺 GlobalMedia
|
||||
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Telegram/Telegram.list, 📟 Telegram
|
||||
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/OpenAI/OpenAI.list, 🤖 AI
|
||||
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Gemini/Gemini.list, 🤖 AI
|
||||
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Copilot/Copilot.list, 🤖 AI
|
||||
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Claude/Claude.list, 🤖 AI
|
||||
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Crypto/Crypto.list, 🪙 Crypto
|
||||
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Cryptocurrency/Cryptocurrency.list, 🪙 Crypto
|
||||
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Game/Game.list, 🎮 Game
|
||||
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Global/Global_All_No_Resolve.list, 🚀 Proxy
|
||||
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/ChinaMax/ChinaMax_All_No_Resolve.list, 🇨🇳 China
|
||||
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Lan/Lan.list, 🎯 Direct
|
||||
|
||||
GEOIP, CN, 🇨🇳 China
|
||||
FINAL, 🐠 Final, dns-failed
|
||||
|
||||
[URL Rewrite]
|
||||
^https?://(www.)?(g|google).cn https://www.google.com 302
|
||||
^https?:\/\/(www.)?g\.cn https://www.google.com 302
|
||||
^https?:\/\/(www.)?google\.cn https://www.google.com 302
|
||||
@ -4,14 +4,13 @@ import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"fmt"
|
||||
"github.com/perfect-panel/server/pkg/tool"
|
||||
"net/url"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/perfect-panel/server/pkg/adapter/proxy"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
"github.com/perfect-panel/server/pkg/tool"
|
||||
"github.com/perfect-panel/server/pkg/traffic"
|
||||
)
|
||||
|
||||
@ -39,46 +38,26 @@ func NewSurge(adapter proxy.Adapter) *Surge {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Surge) Build(uuid, siteName string, user UserInfo) []byte {
|
||||
var proxies, proxyGroup, rules string
|
||||
func (m *Surge) Build(siteName string, user UserInfo) []byte {
|
||||
var proxies, proxyGroup string
|
||||
var removed []string
|
||||
var ps []string
|
||||
|
||||
for _, p := range m.Adapter.Proxies {
|
||||
switch p.Protocol {
|
||||
case "shadowsocks":
|
||||
proxies += buildShadowsocks(p, uuid)
|
||||
proxies += buildShadowsocks(p, user.UUID)
|
||||
case "trojan":
|
||||
proxies += buildTrojan(p, uuid)
|
||||
proxies += buildTrojan(p, user.UUID)
|
||||
case "hysteria2":
|
||||
proxies += buildHysteria2(p, uuid)
|
||||
proxies += buildHysteria2(p, user.UUID)
|
||||
case "vmess":
|
||||
proxies += buildVMess(p, uuid)
|
||||
proxies += buildVMess(p, user.UUID)
|
||||
default:
|
||||
removed = append(removed, p.Name)
|
||||
}
|
||||
ps = append(ps, p.Name)
|
||||
}
|
||||
for _, group := range m.Adapter.Group {
|
||||
if len(removed) > 0 {
|
||||
group.Proxies = tool.RemoveStringElement(group.Proxies, removed...)
|
||||
}
|
||||
if group.Type == proxy.GroupTypeSelect {
|
||||
proxyGroup += fmt.Sprintf("%s = select, %s", group.Name, strings.Join(group.Proxies, ", ")) + "\r\n"
|
||||
} else if group.Type == proxy.GroupTypeURLTest {
|
||||
proxyGroup += fmt.Sprintf("%s = url-test, %s, url=%s, interval=%d", group.Name, strings.Join(group.Proxies, ", "), group.URL, group.Interval) + "\r\n"
|
||||
} else if group.Type == proxy.GroupTypeFallback {
|
||||
proxyGroup += fmt.Sprintf("%s = fallback, %s, url=%s, interval=%d", group.Name, strings.Join(group.Proxies, ", "), group.URL, group.Interval) + "\r\n"
|
||||
} else {
|
||||
logger.Errorf("[BuildSurfboard] unknown group type: %s", group.Type)
|
||||
}
|
||||
}
|
||||
for _, rule := range m.Adapter.Rules {
|
||||
if rule == "" {
|
||||
continue
|
||||
}
|
||||
rules += rule + "\r\n"
|
||||
}
|
||||
//final rule
|
||||
rules += "\r\n" + "FINAL,手动选择,dns-failed"
|
||||
|
||||
file, err := configFiles.ReadFile("default.tpl")
|
||||
if err != nil {
|
||||
@ -99,23 +78,21 @@ func (m *Surge) Build(uuid, siteName string, user UserInfo) []byte {
|
||||
} else {
|
||||
expiredAt = user.ExpiredDate.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
ps = tool.RemoveStringElement(ps, removed...)
|
||||
proxyGroup = strings.Join(ps, ",")
|
||||
|
||||
// convert traffic
|
||||
upload := traffic.AutoConvert(user.Upload, false)
|
||||
download := traffic.AutoConvert(user.Download, false)
|
||||
total := traffic.AutoConvert(user.TotalTraffic, false)
|
||||
unusedTraffic := traffic.AutoConvert(user.TotalTraffic-user.Upload-user.Download, false)
|
||||
// query Host
|
||||
urlParse, err := url.Parse(user.SubscribeURL)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if err := tpl.Execute(&buf, map[string]interface{}{
|
||||
"Proxies": proxies,
|
||||
"ProxyGroup": proxyGroup,
|
||||
"SubscribeURL": user.SubscribeURL,
|
||||
"SubscribeInfo": fmt.Sprintf("title=%s订阅信息, content=上传流量:%s\\n下载流量:%s\\n剩余流量: %s\\n套餐流量:%s\\n到期时间:%s", siteName, upload, download, unusedTraffic, total, expiredAt),
|
||||
"SubscribeDomain": urlParse.Host,
|
||||
"Rules": rules,
|
||||
}); err != nil {
|
||||
logger.Errorf("build Surge config error: %v", err.Error())
|
||||
return nil
|
||||
|
||||
51
pkg/adapter/template/clash.tpl
Normal file
51
pkg/adapter/template/clash.tpl
Normal file
@ -0,0 +1,51 @@
|
||||
mode: rule
|
||||
ipv6: true
|
||||
allow-lan: true
|
||||
bind-address: "*"
|
||||
mixed-port: 7890
|
||||
log-level: error
|
||||
unified-delay: true
|
||||
tcp-concurrent: true
|
||||
external-controller: 0.0.0.0:9090
|
||||
|
||||
tun:
|
||||
enable: true
|
||||
stack: system
|
||||
auto-route: true
|
||||
|
||||
dns:
|
||||
enable: true
|
||||
cache-algorithm: arc
|
||||
listen: 0.0.0.0:1053
|
||||
ipv6: true
|
||||
enhanced-mode: fake-ip
|
||||
fake-ip-range: 198.18.0.1/16
|
||||
fake-ip-filter:
|
||||
- "*.lan"
|
||||
- "lens.l.google.com"
|
||||
- "*.srv.nintendo.net"
|
||||
- "*.stun.playstation.net"
|
||||
- "xbox.*.*.microsoft.com"
|
||||
- "*.xboxlive.com"
|
||||
- "*.msftncsi.com"
|
||||
- "*.msftconnecttest.com"
|
||||
default-nameserver:
|
||||
- 119.29.29.29
|
||||
- 223.5.5.5
|
||||
nameserver:
|
||||
- system
|
||||
- 119.29.29.29
|
||||
- 223.5.5.5
|
||||
fallback:
|
||||
- 8.8.8.8
|
||||
- 1.1.1.1
|
||||
fallback-filter:
|
||||
geoip: true
|
||||
geoip-code: CN
|
||||
|
||||
proxies:
|
||||
{{.Proxies | toYaml | indent 2}}
|
||||
proxy-groups:
|
||||
{{.ProxyGroups | toYaml | indent 2}}
|
||||
rules:
|
||||
{{.Rules | toYaml | indent 2}}
|
||||
@ -2,6 +2,8 @@ package adapter
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/perfect-panel/server/internal/model/server"
|
||||
@ -11,14 +13,21 @@ import (
|
||||
"github.com/perfect-panel/server/pkg/tool"
|
||||
)
|
||||
|
||||
// addNode creates a new proxy node based on the provided server data and host/port.
|
||||
func addNode(data *server.Server, host string, port int) *proxy.Proxy {
|
||||
var option any
|
||||
tags := strings.Split(data.Tags, ",")
|
||||
if len(tags) > 0 {
|
||||
tags = tool.RemoveDuplicateElements(tags...)
|
||||
}
|
||||
|
||||
node := proxy.Proxy{
|
||||
Name: data.Name,
|
||||
Server: host,
|
||||
Port: port,
|
||||
Country: data.Country,
|
||||
Protocol: data.Protocol,
|
||||
Tags: tags,
|
||||
}
|
||||
switch data.Protocol {
|
||||
case "shadowsocks":
|
||||
@ -75,85 +84,78 @@ func addNode(data *server.Server, host string, port int) *proxy.Proxy {
|
||||
node.Port = tuic.Port
|
||||
}
|
||||
option = tuic
|
||||
case "anytls":
|
||||
var anytls proxy.AnyTLS
|
||||
if err := json.Unmarshal([]byte(data.Config), &anytls); err != nil {
|
||||
logger.Errorw("解析AnyTLS配置失败", logger.Field("error", err.Error()), logger.Field("node", data.Name))
|
||||
return nil
|
||||
}
|
||||
if port == 0 {
|
||||
node.Port = anytls.Port
|
||||
}
|
||||
option = anytls
|
||||
logger.Infow("成功处理AnyTLS节点", logger.Field("node", data.Name), logger.Field("port", anytls.Port))
|
||||
default:
|
||||
fmt.Printf("[Error] 不支持的协议: %s", data.Protocol)
|
||||
return nil
|
||||
}
|
||||
node.Option = option
|
||||
return &node
|
||||
}
|
||||
|
||||
func addProxyToGroup(proxyName, groupName string, groups []proxy.Group) []proxy.Group {
|
||||
for i, group := range groups {
|
||||
if group.Name == groupName {
|
||||
groups[i].Proxies = tool.RemoveDuplicateElements(append(group.Proxies, proxyName)...)
|
||||
return groups
|
||||
}
|
||||
}
|
||||
groups = append(groups, proxy.Group{
|
||||
Name: groupName,
|
||||
Type: proxy.GroupTypeSelect,
|
||||
Proxies: []string{proxyName},
|
||||
})
|
||||
return groups
|
||||
}
|
||||
|
||||
func adapterRules(groups []*server.RuleGroup) (proxyGroup []proxy.Group, rules []string) {
|
||||
func adapterRules(groups []*server.RuleGroup) (proxyGroup []proxy.Group, rules []string, defaultGroup string) {
|
||||
for _, group := range groups {
|
||||
if group.Default {
|
||||
log.Printf("[Debug] 规则组 %s 是默认组", group.Name)
|
||||
defaultGroup = group.Name
|
||||
}
|
||||
switch group.Type {
|
||||
case server.RuleGroupTypeReject:
|
||||
proxyGroup = append(proxyGroup, proxy.Group{
|
||||
Name: group.Name,
|
||||
Type: proxy.GroupTypeSelect,
|
||||
Proxies: RemoveEmptyString(strings.Split(group.Tags, ",")),
|
||||
Proxies: []string{"REJECT", "DIRECT", AutoSelect},
|
||||
Reject: true,
|
||||
})
|
||||
case server.RuleGroupTypeDirect:
|
||||
proxyGroup = append(proxyGroup, proxy.Group{
|
||||
Name: group.Name,
|
||||
Type: proxy.GroupTypeSelect,
|
||||
Proxies: []string{"DIRECT", AutoSelect},
|
||||
Direct: true,
|
||||
})
|
||||
default:
|
||||
proxyGroup = append(proxyGroup, proxy.Group{
|
||||
Name: group.Name,
|
||||
Type: proxy.GroupTypeSelect,
|
||||
Proxies: []string{},
|
||||
Tags: RemoveEmptyString(strings.Split(group.Tags, ",")),
|
||||
Default: group.Default,
|
||||
})
|
||||
}
|
||||
|
||||
rules = append(rules, strings.Split(group.Rules, "\n")...)
|
||||
}
|
||||
return
|
||||
log.Printf("[Dapter] 生成规则组: %d", len(proxyGroup))
|
||||
return proxyGroup, tool.RemoveDuplicateElements(rules...), defaultGroup
|
||||
}
|
||||
|
||||
func adapterTags(tags map[string][]*server.Server, group []proxy.Group) (proxyGroup []proxy.Group) {
|
||||
for tag, servers := range tags {
|
||||
proxies := adapterProxies(servers)
|
||||
if len(proxies) != 0 {
|
||||
for _, p := range proxies {
|
||||
group = addProxyToGroup(p.Name, tag, group)
|
||||
}
|
||||
}
|
||||
}
|
||||
return group
|
||||
}
|
||||
|
||||
func generateProxyGroup(servers []proxy.Proxy) (proxyGroup []proxy.Group, region []string) {
|
||||
// 设置手动选择分组
|
||||
proxyGroup = append(proxyGroup, []proxy.Group{
|
||||
{
|
||||
Name: "智能线路",
|
||||
// generateDefaultGroup generates a default proxy group with auto-selection and manual selection options.
|
||||
func generateDefaultGroup() (proxyGroup []proxy.Group) {
|
||||
proxyGroup = append(proxyGroup, proxy.Group{
|
||||
Name: AutoSelect,
|
||||
Type: proxy.GroupTypeURLTest,
|
||||
Proxies: make([]string, 0),
|
||||
URL: "https://www.gstatic.com/generate_204",
|
||||
Interval: 300,
|
||||
},
|
||||
{
|
||||
Name: "手动选择",
|
||||
Type: proxy.GroupTypeSelect,
|
||||
Proxies: []string{"智能线路"},
|
||||
},
|
||||
}...)
|
||||
})
|
||||
|
||||
for _, node := range servers {
|
||||
if node.Country != "" {
|
||||
proxyGroup = addProxyToGroup(node.Name, node.Country, proxyGroup)
|
||||
region = append(region, node.Country)
|
||||
|
||||
proxyGroup = addProxyToGroup(node.Country, "智能线路", proxyGroup)
|
||||
return proxyGroup
|
||||
}
|
||||
|
||||
proxyGroup = addProxyToGroup(node.Name, "手动选择", proxyGroup)
|
||||
}
|
||||
proxyGroup = addProxyToGroup("DIRECT", "手动选择", proxyGroup)
|
||||
return proxyGroup, tool.RemoveDuplicateElements(region...)
|
||||
}
|
||||
|
||||
func adapterProxies(servers []*server.Server) []proxy.Proxy {
|
||||
func adapterProxies(servers []*server.Server) ([]proxy.Proxy, []string, map[string][]string) {
|
||||
var proxies []proxy.Proxy
|
||||
var tags = make(map[string][]string)
|
||||
for _, node := range servers {
|
||||
switch node.RelayMode {
|
||||
case server.RelayModeAll:
|
||||
@ -168,8 +170,20 @@ func adapterProxies(servers []*server.Server) []proxy.Proxy {
|
||||
continue
|
||||
}
|
||||
if relay.Prefix != "" {
|
||||
n.Name = relay.Prefix + "-" + n.Name
|
||||
n.Name = relay.Prefix + n.Name
|
||||
}
|
||||
if node.Tags != "" {
|
||||
t := tool.RemoveDuplicateElements(strings.Split(node.Tags, ",")...)
|
||||
for _, tag := range t {
|
||||
if tag != "" {
|
||||
if _, ok := tags[tag]; !ok {
|
||||
tags[tag] = []string{}
|
||||
}
|
||||
tags[tag] = append(tags[tag], n.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
proxies = append(proxies, *n)
|
||||
}
|
||||
case server.RelayModeRandom:
|
||||
@ -185,18 +199,46 @@ func adapterProxies(servers []*server.Server) []proxy.Proxy {
|
||||
continue
|
||||
}
|
||||
if relay.Prefix != "" {
|
||||
n.Name = relay.Prefix + " - " + node.Name
|
||||
n.Name = relay.Prefix + node.Name
|
||||
}
|
||||
if node.Tags != "" {
|
||||
t := tool.RemoveDuplicateElements(strings.Split(node.Tags, ",")...)
|
||||
for _, tag := range t {
|
||||
if tag != "" {
|
||||
if _, ok := tags[tag]; !ok {
|
||||
tags[tag] = []string{}
|
||||
}
|
||||
tags[tag] = append(tags[tag], n.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
proxies = append(proxies, *n)
|
||||
default:
|
||||
logger.Info("Not Relay Mode", logger.Field("node", node.Name), logger.Field("relayMode", node.RelayMode))
|
||||
n := addNode(node, node.ServerAddr, 0)
|
||||
if n != nil {
|
||||
if node.Tags != "" {
|
||||
t := tool.RemoveDuplicateElements(strings.Split(node.Tags, ",")...)
|
||||
for _, tag := range t {
|
||||
if tag != "" {
|
||||
if _, ok := tags[tag]; !ok {
|
||||
tags[tag] = []string{}
|
||||
}
|
||||
tags[tag] = append(tags[tag], n.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
proxies = append(proxies, *n)
|
||||
}
|
||||
}
|
||||
}
|
||||
return proxies
|
||||
|
||||
var nodes []string
|
||||
for _, p := range proxies {
|
||||
nodes = append(nodes, p.Name)
|
||||
}
|
||||
|
||||
return proxies, tool.RemoveDuplicateElements(nodes...), tags
|
||||
}
|
||||
|
||||
// RemoveEmptyString 切片去除空值
|
||||
@ -210,18 +252,61 @@ func RemoveEmptyString(arr []string) []string {
|
||||
return result
|
||||
}
|
||||
|
||||
func RemoveEmptyGroup(arr []proxy.Group) []proxy.Group {
|
||||
var result []proxy.Group
|
||||
var removeNames []string
|
||||
for _, group := range arr {
|
||||
if group.Name == "手动选择" {
|
||||
group.Proxies = tool.RemoveStringElement(group.Proxies, removeNames...)
|
||||
// SortGroups sorts the provided slice of proxy groups by their names.
|
||||
func SortGroups(groups []proxy.Group, nodes []string, tags map[string][]string, defaultName string) []proxy.Group {
|
||||
var sortedGroups []proxy.Group
|
||||
var defaultGroup, autoSelectGroup proxy.Group
|
||||
// 在所有分组找到默认分组并将他放到第一个
|
||||
for _, group := range groups {
|
||||
if group.Name == "" || group.Name == "DIRECT" || group.Name == "REJECT" {
|
||||
continue
|
||||
}
|
||||
if len(group.Proxies) > 0 {
|
||||
result = append(result, group)
|
||||
} else {
|
||||
removeNames = append(removeNames, group.Name)
|
||||
// 如果是默认分组
|
||||
if group.Default {
|
||||
group.Proxies = append([]string{AutoSelect}, nodes...)
|
||||
group.Proxies = append(group.Proxies, "DIRECT")
|
||||
defaultGroup = group
|
||||
continue
|
||||
}
|
||||
if group.Reject || group.Direct {
|
||||
if defaultName != AutoSelect {
|
||||
group.Proxies = append(group.Proxies, defaultName)
|
||||
}
|
||||
sortedGroups = append(sortedGroups, group)
|
||||
continue
|
||||
}
|
||||
|
||||
if group.Name == AutoSelect {
|
||||
group.Proxies = nodes
|
||||
autoSelectGroup = group
|
||||
continue
|
||||
}
|
||||
// Tags 分组
|
||||
if len(group.Tags) > 0 {
|
||||
var proxies []string
|
||||
for _, tag := range group.Tags {
|
||||
if node, ok := tags[tag]; ok {
|
||||
proxies = append(proxies, node...)
|
||||
}
|
||||
}
|
||||
return result
|
||||
group.Proxies = append(tool.RemoveDuplicateElements(proxies...), AutoSelect, "DIRECT")
|
||||
sortedGroups = append(sortedGroups, group)
|
||||
continue
|
||||
}
|
||||
|
||||
group.Proxies = append([]string{AutoSelect}, nodes...)
|
||||
group.Proxies = append(group.Proxies, "DIRECT")
|
||||
group.Proxies = tool.RemoveElementBySlice(group.Proxies, group.Name)
|
||||
sortedGroups = append(sortedGroups, group)
|
||||
}
|
||||
|
||||
if defaultGroup.Name != "" {
|
||||
sortedGroups = append([]proxy.Group{defaultGroup}, sortedGroups...)
|
||||
}
|
||||
if autoSelectGroup.Name != "" && autoSelectGroup.Name != defaultGroup.Name {
|
||||
sortedGroups = append(sortedGroups, autoSelectGroup)
|
||||
}
|
||||
|
||||
return sortedGroups
|
||||
|
||||
}
|
||||
|
||||
@ -340,6 +340,11 @@ func (dm *DeviceManager) Broadcast(message string) {
|
||||
|
||||
}
|
||||
|
||||
// GetOnlineDeviceCount returns the total number of online devices
|
||||
func (dm *DeviceManager) GetOnlineDeviceCount() int64 {
|
||||
return int64(atomic.LoadInt32(&dm.totalOnline))
|
||||
}
|
||||
|
||||
// Gracefully shut down all WebSocket connections
|
||||
func (dm *DeviceManager) Shutdown(ctx context.Context) {
|
||||
<-ctx.Done()
|
||||
|
||||
@ -282,7 +282,6 @@ const (
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
DefaultTrafficExceedEmailTemplate = `<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
|
||||
@ -33,4 +33,7 @@ func RegisterHandlers(mux *asynq.ServeMux, serverCtx *svc.ServiceContext) {
|
||||
|
||||
// Schedule total server data
|
||||
mux.Handle(types.SchedulerTotalServerData, traffic.NewServerDataLogic(serverCtx))
|
||||
|
||||
// Schedule reset traffic
|
||||
mux.Handle(types.SchedulerResetTraffic, traffic.NewResetTrafficLogic(serverCtx))
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
675
queue/logic/order/activateOrderLogic.go_bak
Normal file
675
queue/logic/order/activateOrderLogic.go_bak
Normal file
@ -0,0 +1,675 @@
|
||||
package orderLogic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/perfect-panel/server/pkg/constant"
|
||||
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/perfect-panel/server/internal/config"
|
||||
"github.com/perfect-panel/server/internal/logic/telegram"
|
||||
"github.com/perfect-panel/server/internal/model/order"
|
||||
"github.com/perfect-panel/server/internal/model/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/pkg/tool"
|
||||
"github.com/perfect-panel/server/pkg/uuidx"
|
||||
"github.com/perfect-panel/server/queue/types"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
Subscribe = 1
|
||||
Renewal = 2
|
||||
ResetTraffic = 3
|
||||
Recharge = 4
|
||||
)
|
||||
|
||||
type ActivateOrderLogic struct {
|
||||
svc *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewActivateOrderLogic(svc *svc.ServiceContext) *ActivateOrderLogic {
|
||||
return &ActivateOrderLogic{
|
||||
svc: svc,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *ActivateOrderLogic) ProcessTask(ctx context.Context, task *asynq.Task) error {
|
||||
payload := types.ForthwithActivateOrderPayload{}
|
||||
if err := json.Unmarshal(task.Payload(), &payload); err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Unmarshal payload failed",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("payload", string(task.Payload())),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
// Find order by order no
|
||||
orderInfo, err := l.svc.OrderModel.FindOneByOrderNo(ctx, payload.OrderNo)
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Find order failed",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("order_no", payload.OrderNo),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
// 1: Pending, 2: Paid, 3:Close, 4: Failed, 5:Finished
|
||||
if orderInfo.Status != 2 {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Order status error",
|
||||
logger.Field("order_no", orderInfo.OrderNo),
|
||||
logger.Field("status", orderInfo.Status),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
switch orderInfo.Type {
|
||||
case Subscribe:
|
||||
err = l.NewPurchase(ctx, orderInfo)
|
||||
case Renewal:
|
||||
err = l.Renewal(ctx, orderInfo)
|
||||
case ResetTraffic:
|
||||
err = l.ResetTraffic(ctx, orderInfo)
|
||||
case Recharge:
|
||||
err = l.Recharge(ctx, orderInfo)
|
||||
default:
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Order type is invalid", logger.Field("type", orderInfo.Type))
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Process task failed", logger.Field("error", err.Error()))
|
||||
return nil
|
||||
}
|
||||
// if coupon is not empty
|
||||
if orderInfo.Coupon != "" {
|
||||
// update coupon status
|
||||
err = l.svc.CouponModel.UpdateCount(ctx, orderInfo.Coupon)
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Update coupon status failed",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("coupon", orderInfo.Coupon),
|
||||
)
|
||||
}
|
||||
}
|
||||
// update order status
|
||||
orderInfo.Status = 5
|
||||
err = l.svc.OrderModel.Update(ctx, orderInfo)
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Update order status failed",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("order_no", orderInfo.OrderNo),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewPurchase New purchase
|
||||
func (l *ActivateOrderLogic) NewPurchase(ctx context.Context, orderInfo *order.Order) error {
|
||||
var userInfo *user.User
|
||||
var err error
|
||||
if orderInfo.UserId != 0 {
|
||||
// find user by user id
|
||||
userInfo, err = l.svc.UserModel.FindOne(ctx, orderInfo.UserId)
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Find user failed",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("user_id", orderInfo.UserId),
|
||||
)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// If User ID is 0, it means that the order is a guest order, need to create a new user
|
||||
// query info with redis
|
||||
cacheKey := fmt.Sprintf(constant.TempOrderCacheKey, orderInfo.OrderNo)
|
||||
data, err := l.svc.Redis.Get(ctx, cacheKey).Result()
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Get temp order cache failed",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("cache_key", cacheKey),
|
||||
)
|
||||
return err
|
||||
}
|
||||
var tempOrder constant.TemporaryOrderInfo
|
||||
if err = json.Unmarshal([]byte(data), &tempOrder); err != nil {
|
||||
logger.WithContext(ctx).Errorw("[ActivateOrderLogic] Unmarshal temp order failed",
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return err
|
||||
}
|
||||
// create user
|
||||
|
||||
userInfo = &user.User{
|
||||
Password: tool.EncodePassWord(tempOrder.Password),
|
||||
AuthMethods: []user.AuthMethods{
|
||||
{
|
||||
AuthType: tempOrder.AuthType,
|
||||
AuthIdentifier: tempOrder.Identifier,
|
||||
},
|
||||
},
|
||||
}
|
||||
err = l.svc.UserModel.Transaction(ctx, func(tx *gorm.DB) error {
|
||||
// Save user information
|
||||
if err := tx.Save(userInfo).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// Generate ReferCode
|
||||
userInfo.ReferCode = uuidx.UserInviteCode(userInfo.Id)
|
||||
// Update ReferCode
|
||||
if err := tx.Model(&user.User{}).Where("id = ?", userInfo.Id).Update("refer_code", userInfo.ReferCode).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
orderInfo.UserId = userInfo.Id
|
||||
return tx.Model(&order.Order{}).Where("order_no = ?", orderInfo.OrderNo).Update("user_id", userInfo.Id).Error
|
||||
})
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Create user failed",
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
if tempOrder.InviteCode != "" {
|
||||
// find referer by refer code
|
||||
referer, err := l.svc.UserModel.FindOneByReferCode(ctx, tempOrder.InviteCode)
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Find referer failed",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("refer_code", tempOrder.InviteCode),
|
||||
)
|
||||
} else {
|
||||
userInfo.RefererId = referer.Id
|
||||
err = l.svc.UserModel.Update(ctx, userInfo)
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Update user referer failed",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.WithContext(ctx).Info("[ActivateOrderLogic] Create guest user success", logger.Field("user_id", userInfo.Id), logger.Field("Identifier", tempOrder.Identifier), logger.Field("AuthType", tempOrder.AuthType))
|
||||
}
|
||||
// find subscribe by id
|
||||
sub, err := l.svc.SubscribeModel.FindOne(ctx, orderInfo.SubscribeId)
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Errorw("[ActivateOrderLogic] Find subscribe failed",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("subscribe_id", orderInfo.SubscribeId),
|
||||
)
|
||||
return err
|
||||
}
|
||||
// create user subscribe
|
||||
now := time.Now()
|
||||
|
||||
userSub := user.Subscribe{
|
||||
Id: 0,
|
||||
UserId: orderInfo.UserId,
|
||||
OrderId: orderInfo.Id,
|
||||
SubscribeId: orderInfo.SubscribeId,
|
||||
StartTime: now,
|
||||
ExpireTime: tool.AddTime(sub.UnitTime, orderInfo.Quantity, now),
|
||||
Traffic: sub.Traffic,
|
||||
Download: 0,
|
||||
Upload: 0,
|
||||
Token: uuidx.SubscribeToken(orderInfo.OrderNo),
|
||||
UUID: uuid.New().String(),
|
||||
Status: 1,
|
||||
}
|
||||
err = l.svc.UserModel.InsertSubscribe(ctx, &userSub)
|
||||
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Insert user subscribe failed",
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return err
|
||||
}
|
||||
// handler commission
|
||||
if userInfo.RefererId != 0 &&
|
||||
l.svc.Config.Invite.ReferralPercentage != 0 &&
|
||||
(!l.svc.Config.Invite.OnlyFirstPurchase || orderInfo.IsNew) {
|
||||
referer, err := l.svc.UserModel.FindOne(ctx, userInfo.RefererId)
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Find referer failed",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("referer_id", userInfo.RefererId),
|
||||
)
|
||||
goto updateCache
|
||||
}
|
||||
// calculate commission
|
||||
amount := float64(orderInfo.Price) * (float64(l.svc.Config.Invite.ReferralPercentage) / 100)
|
||||
referer.Commission += int64(amount)
|
||||
err = l.svc.UserModel.Update(ctx, referer)
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Update referer commission failed",
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
goto updateCache
|
||||
}
|
||||
// create commission log
|
||||
commissionLog := user.CommissionLog{
|
||||
UserId: referer.Id,
|
||||
OrderNo: orderInfo.OrderNo,
|
||||
Amount: int64(amount),
|
||||
}
|
||||
err = l.svc.UserModel.InsertCommissionLog(ctx, &commissionLog)
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Insert commission log failed",
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
}
|
||||
err = l.svc.UserModel.UpdateUserCache(ctx, referer)
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Errorw("[ActivateOrderLogic] Update referer cache", logger.Field("error", err.Error()), logger.Field("user_id", referer.Id))
|
||||
}
|
||||
}
|
||||
updateCache:
|
||||
for _, id := range tool.StringToInt64Slice(sub.Server) {
|
||||
cacheKey := fmt.Sprintf("%s%d", config.ServerUserListCacheKey, id)
|
||||
err = l.svc.Redis.Del(ctx, cacheKey).Err()
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Del server user list cache failed",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("cache_key", cacheKey),
|
||||
)
|
||||
}
|
||||
}
|
||||
data, err := l.svc.ServerModel.FindServerListByGroupIds(ctx, tool.StringToInt64Slice(sub.ServerGroup))
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Find server list failed", logger.Field("error", err.Error()))
|
||||
return err
|
||||
}
|
||||
for _, item := range data {
|
||||
cacheKey := fmt.Sprintf("%s%d", config.ServerUserListCacheKey, item.Id)
|
||||
err = l.svc.Redis.Del(ctx, cacheKey).Err()
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Del server user list cache failed",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("cache_key", cacheKey),
|
||||
)
|
||||
}
|
||||
}
|
||||
userTelegramChatId, ok := findTelegram(userInfo)
|
||||
|
||||
// sendMessage To Telegram
|
||||
if ok {
|
||||
text, err := tool.RenderTemplateToString(telegram.PurchaseNotify, map[string]string{
|
||||
"OrderNo": orderInfo.OrderNo,
|
||||
"SubscribeName": sub.Name,
|
||||
"OrderAmount": fmt.Sprintf("%.2f", float64(orderInfo.Price)/100),
|
||||
"ExpireTime": userSub.ExpireTime.Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Render template failed",
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
}
|
||||
l.sendUserNotifyWithTelegram(userTelegramChatId, text)
|
||||
}
|
||||
// send message to admin
|
||||
text, err := tool.RenderTemplateToString(telegram.AdminOrderNotify, map[string]string{
|
||||
"OrderNo": orderInfo.OrderNo,
|
||||
"TradeNo": orderInfo.TradeNo,
|
||||
"SubscribeName": sub.Name,
|
||||
//"UserEmail": userInfo.Email,
|
||||
"OrderAmount": fmt.Sprintf("%.2f", float64(orderInfo.Price)/100),
|
||||
"OrderStatus": "已支付",
|
||||
"OrderTime": orderInfo.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
"PaymentMethod": orderInfo.Method,
|
||||
})
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Render AdminOrderNotify template failed",
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
}
|
||||
l.sendAdminNotifyWithTelegram(ctx, text)
|
||||
logger.WithContext(ctx).Info("[ActivateOrderLogic] Insert user subscribe success")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Renewal Renewal
|
||||
func (l *ActivateOrderLogic) Renewal(ctx context.Context, orderInfo *order.Order) error {
|
||||
// find user by user id
|
||||
userInfo, err := l.svc.UserModel.FindOne(ctx, orderInfo.UserId)
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Find user failed",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("user_id", orderInfo.UserId),
|
||||
)
|
||||
return err
|
||||
}
|
||||
// find user subscribe by subscribe token
|
||||
userSub, err := l.svc.UserModel.FindOneSubscribeByToken(ctx, orderInfo.SubscribeToken)
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Find user subscribe failed",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("order_id", orderInfo.Id),
|
||||
)
|
||||
return err
|
||||
}
|
||||
// find subscribe by id
|
||||
sub, err := l.svc.SubscribeModel.FindOne(ctx, orderInfo.SubscribeId)
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Find subscribe failed",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("subscribe_id", orderInfo.SubscribeId),
|
||||
logger.Field("order_id", orderInfo.Id),
|
||||
)
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
if userSub.ExpireTime.Before(now) {
|
||||
userSub.ExpireTime = now
|
||||
}
|
||||
|
||||
// Check whether traffic reset on renewal is enabled
|
||||
if sub.RenewalReset != nil && *sub.RenewalReset {
|
||||
userSub.Download = 0
|
||||
userSub.Upload = 0
|
||||
}
|
||||
if userSub.FinishedAt != nil {
|
||||
userSub.FinishedAt = nil
|
||||
}
|
||||
|
||||
userSub.ExpireTime = tool.AddTime(sub.UnitTime, orderInfo.Quantity, userSub.ExpireTime)
|
||||
userSub.Status = 1
|
||||
// update user subscribe
|
||||
err = l.svc.UserModel.UpdateSubscribe(ctx, userSub)
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Update user subscribe failed",
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return err
|
||||
}
|
||||
// handler commission
|
||||
if userInfo.RefererId != 0 &&
|
||||
l.svc.Config.Invite.ReferralPercentage != 0 &&
|
||||
!l.svc.Config.Invite.OnlyFirstPurchase {
|
||||
referer, err := l.svc.UserModel.FindOne(ctx, userInfo.RefererId)
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Find referer failed",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("referer_id", userInfo.RefererId),
|
||||
)
|
||||
goto sendMessage
|
||||
}
|
||||
// calculate commission
|
||||
amount := float64(orderInfo.Price) * (float64(l.svc.Config.Invite.ReferralPercentage) / 100)
|
||||
referer.Commission += int64(amount)
|
||||
err = l.svc.UserModel.Update(ctx, referer)
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Update referer commission failed",
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
goto sendMessage
|
||||
}
|
||||
// create commission log
|
||||
commissionLog := user.CommissionLog{
|
||||
UserId: referer.Id,
|
||||
OrderNo: orderInfo.OrderNo,
|
||||
Amount: int64(amount),
|
||||
}
|
||||
err = l.svc.UserModel.InsertCommissionLog(ctx, &commissionLog)
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Insert commission log failed",
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
}
|
||||
err = l.svc.UserModel.UpdateUserCache(ctx, referer)
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Errorw("[ActivateOrderLogic] Update referer cache", logger.Field("error", err.Error()), logger.Field("user_id", referer.Id))
|
||||
}
|
||||
}
|
||||
|
||||
sendMessage:
|
||||
userTelegramChatId, ok := findTelegram(userInfo)
|
||||
// SendMessage To Telegram
|
||||
if ok {
|
||||
text, err := tool.RenderTemplateToString(telegram.RenewalNotify, map[string]string{
|
||||
"OrderNo": orderInfo.OrderNo,
|
||||
"SubscribeName": sub.Name,
|
||||
"OrderAmount": fmt.Sprintf("%.2f", float64(orderInfo.Price)/100),
|
||||
"ExpireTime": userSub.ExpireTime.Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Render template failed",
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
}
|
||||
l.sendUserNotifyWithTelegram(userTelegramChatId, text)
|
||||
}
|
||||
|
||||
// send message to admin
|
||||
text, err := tool.RenderTemplateToString(telegram.AdminOrderNotify, map[string]string{
|
||||
"OrderNo": orderInfo.OrderNo,
|
||||
"TradeNo": orderInfo.TradeNo,
|
||||
"SubscribeName": sub.Name,
|
||||
//"UserEmail": userInfo.Email,
|
||||
"OrderAmount": fmt.Sprintf("%.2f", float64(orderInfo.Price)/100),
|
||||
"OrderStatus": "已支付",
|
||||
"OrderTime": orderInfo.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
"PaymentMethod": orderInfo.Method,
|
||||
})
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Render AdminOrderNotify template failed",
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
}
|
||||
l.sendAdminNotifyWithTelegram(ctx, text)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetTraffic Reset traffic
|
||||
func (l *ActivateOrderLogic) ResetTraffic(ctx context.Context, orderInfo *order.Order) error {
|
||||
// find user by user id
|
||||
userInfo, err := l.svc.UserModel.FindOne(ctx, orderInfo.UserId)
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Find user failed",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("user_id", orderInfo.UserId),
|
||||
)
|
||||
return err
|
||||
}
|
||||
// Generate a Subscribe Token through orderNo
|
||||
// find user subscribe by subscribe token
|
||||
userSub, err := l.svc.UserModel.FindOneSubscribeByToken(ctx, orderInfo.SubscribeToken)
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Find user subscribe failed",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("order_id", orderInfo.Id),
|
||||
)
|
||||
return err
|
||||
}
|
||||
userSub.Download = 0
|
||||
userSub.Upload = 0
|
||||
userSub.Status = 1
|
||||
// update user subscribe
|
||||
err = l.svc.UserModel.UpdateSubscribe(ctx, userSub)
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Update user subscribe failed",
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return err
|
||||
}
|
||||
sub, err := l.svc.SubscribeModel.FindOne(ctx, userSub.SubscribeId)
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Find subscribe failed",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("subscribe_id", userSub.SubscribeId),
|
||||
)
|
||||
return err
|
||||
}
|
||||
userTelegramChatId, ok := findTelegram(userInfo)
|
||||
// SendMessage To Telegram
|
||||
if ok {
|
||||
text, err := tool.RenderTemplateToString(telegram.ResetTrafficNotify, map[string]string{
|
||||
"OrderNo": orderInfo.OrderNo,
|
||||
"SubscribeName": sub.Name,
|
||||
"ResetTime": time.Now().Format("2006-01-02 15:04:05"),
|
||||
"ExpireTime": userSub.ExpireTime.Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Render template failed",
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
}
|
||||
l.sendUserNotifyWithTelegram(userTelegramChatId, text)
|
||||
}
|
||||
|
||||
// send message to admin
|
||||
text, err := tool.RenderTemplateToString(telegram.AdminOrderNotify, map[string]string{
|
||||
"OrderNo": orderInfo.OrderNo,
|
||||
"TradeNo": orderInfo.TradeNo,
|
||||
"SubscribeName": "流量重置",
|
||||
"OrderAmount": fmt.Sprintf("%.2f", float64(orderInfo.Price)/100),
|
||||
"OrderStatus": "已支付",
|
||||
"OrderTime": orderInfo.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
"PaymentMethod": orderInfo.Method,
|
||||
})
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Render AdminOrderNotify template failed",
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
}
|
||||
l.sendAdminNotifyWithTelegram(ctx, text)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Recharge Recharge to user
|
||||
func (l *ActivateOrderLogic) Recharge(ctx context.Context, orderInfo *order.Order) error {
|
||||
// find user by user id
|
||||
userInfo, err := l.svc.UserModel.FindOne(ctx, orderInfo.UserId)
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Find user failed",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("user_id", orderInfo.UserId),
|
||||
)
|
||||
return err
|
||||
}
|
||||
userInfo.Balance += orderInfo.Price
|
||||
// update user
|
||||
err = l.svc.DB.Transaction(func(tx *gorm.DB) error {
|
||||
err = l.svc.UserModel.Update(ctx, userInfo, tx)
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Update user failed",
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return err
|
||||
}
|
||||
// Create Balance Log
|
||||
balanceLog := user.BalanceLog{
|
||||
UserId: orderInfo.UserId,
|
||||
Amount: orderInfo.Price,
|
||||
Type: 1,
|
||||
OrderId: orderInfo.Id,
|
||||
Balance: userInfo.Balance,
|
||||
}
|
||||
err = l.svc.UserModel.InsertBalanceLog(ctx, &balanceLog, tx)
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Insert balance log failed",
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Database transaction failed",
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return err
|
||||
}
|
||||
userTelegramChatId, ok := findTelegram(userInfo)
|
||||
// SendMessage To Telegram
|
||||
if ok {
|
||||
text, err := tool.RenderTemplateToString(telegram.RechargeNotify, map[string]string{
|
||||
"OrderAmount": fmt.Sprintf("%.2f", float64(orderInfo.Price)/100),
|
||||
"PaymentMethod": orderInfo.Method,
|
||||
"Time": orderInfo.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
"Balance": fmt.Sprintf("%.2f", float64(userInfo.Balance)/100),
|
||||
})
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Render template failed",
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
}
|
||||
l.sendUserNotifyWithTelegram(userTelegramChatId, text)
|
||||
}
|
||||
// send message to admin
|
||||
text, err := tool.RenderTemplateToString(telegram.AdminOrderNotify, map[string]string{
|
||||
"OrderNo": orderInfo.OrderNo,
|
||||
"TradeNo": orderInfo.TradeNo,
|
||||
"OrderAmount": fmt.Sprintf("%.2f", float64(orderInfo.Price)/100),
|
||||
"SubscribeName": "余额充值",
|
||||
"OrderStatus": "已支付",
|
||||
"OrderTime": orderInfo.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
"PaymentMethod": orderInfo.Method,
|
||||
})
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Render AdminOrderNotify template failed",
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
}
|
||||
l.sendAdminNotifyWithTelegram(ctx, text)
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendUserNotifyWithTelegram send message to user
|
||||
func (l *ActivateOrderLogic) sendUserNotifyWithTelegram(chatId int64, text string) {
|
||||
msg := tgbotapi.NewMessage(chatId, text)
|
||||
msg.ParseMode = "markdown"
|
||||
_, err := l.svc.TelegramBot.Send(msg)
|
||||
if err != nil {
|
||||
logger.Error("[ActivateOrderLogic] Send telegram user message failed",
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// sendAdminNotifyWithTelegram send message to admin
|
||||
func (l *ActivateOrderLogic) sendAdminNotifyWithTelegram(ctx context.Context, text string) {
|
||||
admins, err := l.svc.UserModel.QueryAdminUsers(ctx)
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Query admin users failed",
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return
|
||||
}
|
||||
for _, admin := range admins {
|
||||
telegramId, ok := findTelegram(admin)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
msg := tgbotapi.NewMessage(telegramId, text)
|
||||
msg.ParseMode = "markdown"
|
||||
_, err := l.svc.TelegramBot.Send(msg)
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] Send telegram admin message failed",
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// findTelegram find user telegram id
|
||||
func findTelegram(u *user.User) (int64, bool) {
|
||||
for _, item := range u.AuthMethods {
|
||||
if item.AuthType == "telegram" {
|
||||
// string to int64
|
||||
parseInt, err := strconv.ParseInt(item.AuthIdentifier, 10, 64)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
return parseInt, true
|
||||
}
|
||||
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
540
queue/logic/traffic/resetTrafficLogic.go
Normal file
540
queue/logic/traffic/resetTrafficLogic.go
Normal file
@ -0,0 +1,540 @@
|
||||
package traffic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/perfect-panel/server/internal/model/subscribe"
|
||||
"github.com/perfect-panel/server/internal/model/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
"github.com/perfect-panel/server/queue/types"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ResetTrafficLogic handles traffic reset logic for different subscription cycles
|
||||
// Supports three reset modes:
|
||||
// - reset_cycle = 1: Reset on 1st of every month
|
||||
// - reset_cycle = 2: Reset monthly based on subscription start date
|
||||
// - reset_cycle = 3: Reset yearly based on subscription start date
|
||||
type ResetTrafficLogic struct {
|
||||
svc *svc.ServiceContext
|
||||
}
|
||||
|
||||
// Cache and retry configuration constants
|
||||
const (
|
||||
maxRetryAttempts = 3
|
||||
retryDelay = 30 * time.Minute
|
||||
lockTimeout = 5 * time.Minute
|
||||
)
|
||||
|
||||
// Cache keys
|
||||
var (
|
||||
cacheKey = "reset_traffic_cache"
|
||||
retryCountKey = "reset_traffic_retry_count"
|
||||
lockKey = "reset_traffic_lock"
|
||||
)
|
||||
|
||||
// resetTrafficCache stores the last reset time to prevent duplicate processing
|
||||
type resetTrafficCache struct {
|
||||
LastResetTime time.Time
|
||||
}
|
||||
|
||||
func NewResetTrafficLogic(svc *svc.ServiceContext) *ResetTrafficLogic {
|
||||
return &ResetTrafficLogic{
|
||||
svc: svc,
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessTask executes the traffic reset task for all subscription types with enhanced retry mechanism
|
||||
func (l *ResetTrafficLogic) ProcessTask(ctx context.Context, _ *asynq.Task) error {
|
||||
var err error
|
||||
startTime := time.Now()
|
||||
|
||||
// Get current retry count
|
||||
retryCount := l.getRetryCount(ctx)
|
||||
logger.Infow("[ResetTraffic] Starting task execution",
|
||||
logger.Field("retryCount", retryCount),
|
||||
logger.Field("startTime", startTime))
|
||||
|
||||
// Acquire distributed lock to prevent duplicate execution
|
||||
lockAcquired := l.acquireLock(ctx)
|
||||
if !lockAcquired {
|
||||
logger.Infow("[ResetTraffic] Another task is already running, skipping execution")
|
||||
return nil
|
||||
}
|
||||
defer l.releaseLock(ctx)
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
// Check if error is retryable and within retry limit
|
||||
if l.isRetryableError(err) && retryCount < maxRetryAttempts {
|
||||
// Increment retry count
|
||||
l.setRetryCount(ctx, retryCount+1)
|
||||
|
||||
// Schedule retry with delay
|
||||
task := asynq.NewTask(types.SchedulerResetTraffic, nil)
|
||||
_, retryErr := l.svc.Queue.Enqueue(task, asynq.ProcessIn(retryDelay))
|
||||
if retryErr != nil {
|
||||
logger.Errorw("[ResetTraffic] Failed to enqueue retry task",
|
||||
logger.Field("error", retryErr.Error()),
|
||||
logger.Field("retryCount", retryCount))
|
||||
} else {
|
||||
logger.Infow("[ResetTraffic] Task failed, retrying in 30 minutes",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("retryCount", retryCount+1),
|
||||
logger.Field("maxRetryAttempts", maxRetryAttempts))
|
||||
}
|
||||
} else {
|
||||
// Max retries reached or non-retryable error
|
||||
if retryCount >= maxRetryAttempts {
|
||||
logger.Errorw("[ResetTraffic] Max retry attempts reached, giving up",
|
||||
logger.Field("retryCount", retryCount),
|
||||
logger.Field("maxRetryAttempts", maxRetryAttempts),
|
||||
logger.Field("error", err.Error()))
|
||||
} else {
|
||||
logger.Errorw("[ResetTraffic] Non-retryable error, not retrying",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("retryCount", retryCount))
|
||||
}
|
||||
// Reset retry count for next scheduled task
|
||||
l.clearRetryCount(ctx)
|
||||
}
|
||||
} else {
|
||||
// Task completed successfully, reset retry count
|
||||
l.clearRetryCount(ctx)
|
||||
logger.Infow("[ResetTraffic] Task completed successfully",
|
||||
logger.Field("processingTime", time.Since(startTime)),
|
||||
logger.Field("retryCount", retryCount))
|
||||
}
|
||||
}()
|
||||
|
||||
// Load last reset time from cache
|
||||
var cache resetTrafficCache
|
||||
err = l.svc.Redis.Get(ctx, cacheKey).Scan(&cache)
|
||||
if err != nil {
|
||||
if !errors.Is(err, redis.Nil) {
|
||||
logger.Errorw("[ResetTraffic] Failed to get cache", logger.Field("error", err.Error()))
|
||||
}
|
||||
// Set default value if cache not found
|
||||
cache = resetTrafficCache{
|
||||
LastResetTime: time.Now().Add(-10 * time.Minute),
|
||||
}
|
||||
logger.Infow("[ResetTraffic] Using default cache value", logger.Field("lastResetTime", cache.LastResetTime))
|
||||
} else {
|
||||
logger.Infow("[ResetTraffic] Cache loaded successfully", logger.Field("lastResetTime", cache.LastResetTime))
|
||||
}
|
||||
|
||||
// Execute reset operations in order: yearly -> monthly (1st) -> monthly (cycle)
|
||||
err = l.resetYear(ctx)
|
||||
if err != nil {
|
||||
logger.Errorw("[ResetTraffic] Yearly reset failed", logger.Field("error", err.Error()))
|
||||
return err
|
||||
}
|
||||
|
||||
err = l.reset1st(ctx, cache)
|
||||
if err != nil {
|
||||
logger.Errorw("[ResetTraffic] Monthly 1st reset failed", logger.Field("error", err.Error()))
|
||||
return err
|
||||
}
|
||||
|
||||
err = l.resetMonth(ctx)
|
||||
if err != nil {
|
||||
logger.Errorw("[ResetTraffic] Monthly cycle reset failed", logger.Field("error", err.Error()))
|
||||
return err
|
||||
}
|
||||
|
||||
// Update cache with current time after successful processing
|
||||
updatedCache := resetTrafficCache{
|
||||
LastResetTime: startTime,
|
||||
}
|
||||
cacheErr := l.svc.Redis.Set(ctx, cacheKey, updatedCache, 0).Err()
|
||||
if cacheErr != nil {
|
||||
logger.Errorw("[ResetTraffic] Failed to update cache", logger.Field("error", cacheErr.Error()))
|
||||
// Don't return error here as the main task completed successfully
|
||||
} else {
|
||||
logger.Infow("[ResetTraffic] Cache updated successfully", logger.Field("newLastResetTime", startTime))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// resetMonth handles monthly cycle reset based on subscription start date
|
||||
// reset_cycle = 2: Reset monthly based on subscription start date
|
||||
func (l *ResetTrafficLogic) resetMonth(ctx context.Context) error {
|
||||
now := time.Now()
|
||||
|
||||
err := l.svc.UserModel.Transaction(ctx, func(db *gorm.DB) error {
|
||||
// Get all subscriptions that reset monthly based on start date
|
||||
var resetMonthSubIds []int64
|
||||
err := db.Model(&subscribe.Subscribe{}).Select("`id`").Where("`reset_cycle` = ?", 2).Find(&resetMonthSubIds).Error
|
||||
if err != nil {
|
||||
logger.Errorw("[ResetTraffic] Failed to query monthly subscriptions", logger.Field("error", err.Error()))
|
||||
return err
|
||||
}
|
||||
|
||||
if len(resetMonthSubIds) == 0 {
|
||||
logger.Infow("[ResetTraffic] No monthly cycle subscriptions found")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Query users for monthly reset based on subscription start date cycle
|
||||
var monthlyResetUsers []int64
|
||||
|
||||
// Check if today is the last day of current month
|
||||
nextMonth := now.AddDate(0, 1, 0)
|
||||
isLastDayOfMonth := nextMonth.Month() != now.Month()
|
||||
|
||||
query := db.Model(&user.Subscribe{}).Select("`id`").
|
||||
Where("`subscribe_id` IN ?", resetMonthSubIds).
|
||||
Where("`status` = ?", 1). // Only active subscriptions
|
||||
Where("PERIOD_DIFF(DATE_FORMAT(CURDATE(), '%Y%m'), DATE_FORMAT(start_time, '%Y%m')) > 0"). // At least one month passed
|
||||
Where("MOD(PERIOD_DIFF(DATE_FORMAT(CURDATE(), '%Y%m'), DATE_FORMAT(start_time, '%Y%m')), 1) = 0"). // Monthly cycle
|
||||
Where("DATE(start_time) < CURDATE()") // Only reset subscriptions that have started
|
||||
|
||||
if isLastDayOfMonth {
|
||||
// Last day of month: handle subscription start dates >= today
|
||||
query = query.Where("DAY(start_time) >= ?", now.Day())
|
||||
} else {
|
||||
// Normal case: exact day match
|
||||
query = query.Where("DAY(start_time) = ?", now.Day())
|
||||
}
|
||||
|
||||
err = query.Find(&monthlyResetUsers).Error
|
||||
if err != nil {
|
||||
logger.Errorw("[ResetTraffic] Failed to query monthly reset users", logger.Field("error", err.Error()))
|
||||
return err
|
||||
}
|
||||
|
||||
if len(monthlyResetUsers) > 0 {
|
||||
logger.Infow("[ResetTraffic] Found users for monthly reset",
|
||||
logger.Field("count", len(monthlyResetUsers)),
|
||||
logger.Field("userIds", monthlyResetUsers))
|
||||
|
||||
err = db.Model(&user.Subscribe{}).Where("`id` IN ?", monthlyResetUsers).
|
||||
Updates(map[string]interface{}{
|
||||
"upload": 0,
|
||||
"download": 0,
|
||||
}).Error
|
||||
if err != nil {
|
||||
logger.Errorw("[ResetTraffic] Failed to update monthly reset users", logger.Field("error", err.Error()))
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Infow("[ResetTraffic] Monthly reset completed", logger.Field("count", len(monthlyResetUsers)))
|
||||
} else {
|
||||
logger.Infow("[ResetTraffic] No users found for monthly reset")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
logger.Errorw("[ResetTraffic] Monthly reset transaction failed", logger.Field("error", err.Error()))
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Infow("[ResetTraffic] Monthly reset process completed")
|
||||
return nil
|
||||
}
|
||||
|
||||
// reset1st handles reset on 1st of every month
|
||||
// reset_cycle = 1: Reset on 1st of every month
|
||||
func (l *ResetTrafficLogic) reset1st(ctx context.Context, cache resetTrafficCache) error {
|
||||
now := time.Now()
|
||||
|
||||
// Check if we already reset this month using cache
|
||||
if cache.LastResetTime.Year() == now.Year() && cache.LastResetTime.Month() == now.Month() {
|
||||
logger.Infow("[ResetTraffic] Already reset this month, skipping 1st reset",
|
||||
logger.Field("lastResetTime", cache.LastResetTime),
|
||||
logger.Field("currentTime", now))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Only reset if it's the 1st day of the month
|
||||
if now.Day() != 1 {
|
||||
logger.Infow("[ResetTraffic] Not 1st day of month, skipping 1st reset", logger.Field("currentDay", now.Day()))
|
||||
return nil
|
||||
}
|
||||
|
||||
err := l.svc.UserModel.Transaction(ctx, func(db *gorm.DB) error {
|
||||
// Get all subscriptions that reset on 1st of month
|
||||
var reset1stSubIds []int64
|
||||
err := db.Model(&subscribe.Subscribe{}).Select("`id`").Where("`reset_cycle` = ?", 1).Find(&reset1stSubIds).Error
|
||||
if err != nil {
|
||||
logger.Errorw("[ResetTraffic] Failed to query 1st reset subscriptions", logger.Field("error", err.Error()))
|
||||
return err
|
||||
}
|
||||
|
||||
if len(reset1stSubIds) == 0 {
|
||||
logger.Infow("[ResetTraffic] No 1st reset subscriptions found")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get all active users with these subscriptions
|
||||
var users1stReset []int64
|
||||
err = db.Model(&user.Subscribe{}).Select("`id`").
|
||||
Where("`subscribe_id` IN ?", reset1stSubIds).
|
||||
Where("`status` = ?", 1). // Only active subscriptions
|
||||
Find(&users1stReset).Error
|
||||
if err != nil {
|
||||
logger.Errorw("[ResetTraffic] Failed to query 1st reset users", logger.Field("error", err.Error()))
|
||||
return err
|
||||
}
|
||||
|
||||
if len(users1stReset) > 0 {
|
||||
logger.Infow("[ResetTraffic] Found users for 1st reset",
|
||||
logger.Field("count", len(users1stReset)),
|
||||
logger.Field("userIds", users1stReset))
|
||||
|
||||
// Reset upload and download traffic to zero
|
||||
err = db.Model(&user.Subscribe{}).Where("`id` IN ?", users1stReset).
|
||||
Updates(map[string]interface{}{
|
||||
"upload": 0,
|
||||
"download": 0,
|
||||
}).Error
|
||||
if err != nil {
|
||||
logger.Errorw("[ResetTraffic] Failed to update 1st reset users", logger.Field("error", err.Error()))
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Infow("[ResetTraffic] 1st reset completed", logger.Field("count", len(users1stReset)))
|
||||
} else {
|
||||
logger.Infow("[ResetTraffic] No users found for 1st reset")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logger.Errorw("[ResetTraffic] 1st reset transaction failed", logger.Field("error", err.Error()))
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Infow("[ResetTraffic] 1st reset process completed")
|
||||
return nil
|
||||
}
|
||||
|
||||
// resetYear handles yearly reset based on subscription start date anniversary
|
||||
// reset_cycle = 3: Reset yearly based on subscription start date
|
||||
func (l *ResetTrafficLogic) resetYear(ctx context.Context) error {
|
||||
now := time.Now()
|
||||
|
||||
err := l.svc.UserModel.Transaction(ctx, func(db *gorm.DB) error {
|
||||
// Get all subscriptions that reset yearly
|
||||
var resetYearSubIds []int64
|
||||
err := db.Model(&subscribe.Subscribe{}).Select("`id`").Where("`reset_cycle` = ?", 3).Find(&resetYearSubIds).Error
|
||||
if err != nil {
|
||||
logger.Errorw("[ResetTraffic] Failed to query yearly subscriptions", logger.Field("error", err.Error()))
|
||||
return err
|
||||
}
|
||||
|
||||
if len(resetYearSubIds) == 0 {
|
||||
logger.Infow("[ResetTraffic] No yearly reset subscriptions found")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Query users for yearly reset based on subscription start date anniversary
|
||||
var usersYearReset []int64
|
||||
|
||||
// Check if today is the last day of current month
|
||||
nextMonth := now.AddDate(0, 1, 0)
|
||||
isLastDayOfMonth := nextMonth.Month() != now.Month()
|
||||
|
||||
// Check if today is February 28th (handle leap year case)
|
||||
isLeapYearCase := now.Month() == 2 && now.Day() == 28
|
||||
|
||||
query := db.Model(&user.Subscribe{}).Select("`id`").
|
||||
Where("`subscribe_id` IN ?", resetYearSubIds).
|
||||
Where("MONTH(start_time) = ?", now.Month()). // Same month
|
||||
Where("`status` = ?", 1). // Only active subscriptions
|
||||
Where("TIMESTAMPDIFF(YEAR, DATE(start_time), CURDATE()) >= 1"). // At least 1 year passed
|
||||
Where("DATE(start_time) < CURDATE()") // Only reset subscriptions that have started
|
||||
|
||||
if isLeapYearCase {
|
||||
// February 28th: handle both Feb 28 and Feb 29 subscriptions
|
||||
query = query.Where("DAY(start_time) IN (28, 29)")
|
||||
} else if isLastDayOfMonth {
|
||||
// Last day of month: handle subscription start dates >= today
|
||||
query = query.Where("DAY(start_time) >= ?", now.Day())
|
||||
} else {
|
||||
// Normal case: exact day match
|
||||
query = query.Where("DAY(start_time) = ?", now.Day())
|
||||
}
|
||||
|
||||
err = query.Find(&usersYearReset).Error
|
||||
if err != nil {
|
||||
logger.Errorw("[ResetTraffic] Query yearly reset users failed", logger.Field("error", err.Error()))
|
||||
return err
|
||||
}
|
||||
|
||||
if len(usersYearReset) > 0 {
|
||||
logger.Infow("[ResetTraffic] Found users for yearly reset",
|
||||
logger.Field("count", len(usersYearReset)),
|
||||
logger.Field("userIds", usersYearReset))
|
||||
|
||||
// Reset upload and download traffic to zero
|
||||
err = db.Model(&user.Subscribe{}).Where("`id` IN ?", usersYearReset).
|
||||
Updates(map[string]interface{}{
|
||||
"upload": 0,
|
||||
"download": 0,
|
||||
}).Error
|
||||
if err != nil {
|
||||
logger.Errorw("[ResetTraffic] Failed to update yearly reset users", logger.Field("error", err.Error()))
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Infow("[ResetTraffic] Yearly reset completed", logger.Field("count", len(usersYearReset)))
|
||||
} else {
|
||||
logger.Infow("[ResetTraffic] No users found for yearly reset")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logger.Errorw("[ResetTraffic] Yearly reset transaction failed", logger.Field("error", err.Error()))
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Infow("[ResetTraffic] Yearly reset process completed")
|
||||
return nil
|
||||
}
|
||||
|
||||
// getRetryCount retrieves the current retry count from Redis
|
||||
func (l *ResetTrafficLogic) getRetryCount(ctx context.Context) int {
|
||||
countStr, err := l.svc.Redis.Get(ctx, retryCountKey).Result()
|
||||
if err != nil {
|
||||
if errors.Is(err, redis.Nil) {
|
||||
return 0 // No retry count found, start with 0
|
||||
}
|
||||
logger.Errorw("[ResetTraffic] Failed to get retry count", logger.Field("error", err.Error()))
|
||||
return 0
|
||||
}
|
||||
|
||||
count, err := strconv.Atoi(countStr)
|
||||
if err != nil {
|
||||
logger.Errorw("[ResetTraffic] Invalid retry count format", logger.Field("value", countStr))
|
||||
return 0
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
// setRetryCount sets the retry count in Redis
|
||||
func (l *ResetTrafficLogic) setRetryCount(ctx context.Context, count int) {
|
||||
err := l.svc.Redis.Set(ctx, retryCountKey, count, 24*time.Hour).Err()
|
||||
if err != nil {
|
||||
logger.Errorw("[ResetTraffic] Failed to set retry count",
|
||||
logger.Field("count", count),
|
||||
logger.Field("error", err.Error()))
|
||||
}
|
||||
}
|
||||
|
||||
// clearRetryCount removes the retry count from Redis
|
||||
func (l *ResetTrafficLogic) clearRetryCount(ctx context.Context) {
|
||||
err := l.svc.Redis.Del(ctx, retryCountKey).Err()
|
||||
if err != nil {
|
||||
logger.Errorw("[ResetTraffic] Failed to clear retry count", logger.Field("error", err.Error()))
|
||||
}
|
||||
}
|
||||
|
||||
// acquireLock attempts to acquire a distributed lock
|
||||
func (l *ResetTrafficLogic) acquireLock(ctx context.Context) bool {
|
||||
result := l.svc.Redis.SetNX(ctx, lockKey, "locked", lockTimeout)
|
||||
acquired, err := result.Result()
|
||||
if err != nil {
|
||||
logger.Errorw("[ResetTraffic] Failed to acquire lock", logger.Field("error", err.Error()))
|
||||
return false
|
||||
}
|
||||
|
||||
if acquired {
|
||||
logger.Infow("[ResetTraffic] Lock acquired successfully")
|
||||
} else {
|
||||
logger.Infow("[ResetTraffic] Lock already exists, another task is running")
|
||||
}
|
||||
|
||||
return acquired
|
||||
}
|
||||
|
||||
// releaseLock releases the distributed lock
|
||||
func (l *ResetTrafficLogic) releaseLock(ctx context.Context) {
|
||||
err := l.svc.Redis.Del(ctx, lockKey).Err()
|
||||
if err != nil {
|
||||
logger.Errorw("[ResetTraffic] Failed to release lock", logger.Field("error", err.Error()))
|
||||
} else {
|
||||
logger.Infow("[ResetTraffic] Lock released successfully")
|
||||
}
|
||||
}
|
||||
|
||||
// isRetryableError determines if an error is retryable
|
||||
func (l *ResetTrafficLogic) isRetryableError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
errorMessage := strings.ToLower(err.Error())
|
||||
|
||||
// Network and connection errors (retryable)
|
||||
retryableErrors := []string{
|
||||
"connection refused",
|
||||
"connection reset",
|
||||
"connection timeout",
|
||||
"network",
|
||||
"timeout",
|
||||
"dial",
|
||||
"context deadline exceeded",
|
||||
"temporary failure",
|
||||
"server error",
|
||||
"service unavailable",
|
||||
"internal server error",
|
||||
"database is locked",
|
||||
"too many connections",
|
||||
"deadlock",
|
||||
"lock wait timeout",
|
||||
}
|
||||
|
||||
// Database constraint errors (non-retryable)
|
||||
nonRetryableErrors := []string{
|
||||
"foreign key constraint",
|
||||
"unique constraint",
|
||||
"check constraint",
|
||||
"not null constraint",
|
||||
"invalid input syntax",
|
||||
"column does not exist",
|
||||
"table does not exist",
|
||||
"permission denied",
|
||||
"access denied",
|
||||
"authentication failed",
|
||||
"invalid credentials",
|
||||
}
|
||||
|
||||
// Check for non-retryable errors first
|
||||
for _, nonRetryable := range nonRetryableErrors {
|
||||
if strings.Contains(errorMessage, nonRetryable) {
|
||||
logger.Infow("[ResetTraffic] Non-retryable error detected",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("pattern", nonRetryable))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Check for retryable errors
|
||||
for _, retryable := range retryableErrors {
|
||||
if strings.Contains(errorMessage, retryable) {
|
||||
logger.Infow("[ResetTraffic] Retryable error detected",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("pattern", retryable))
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Default: treat unknown errors as retryable, but log for analysis
|
||||
logger.Infow("[ResetTraffic] Unknown error type, treating as retryable",
|
||||
logger.Field("error", err.Error()))
|
||||
return true
|
||||
}
|
||||
@ -3,4 +3,5 @@ package types
|
||||
const (
|
||||
SchedulerCheckSubscription = "scheduler:check:subscription"
|
||||
SchedulerTotalServerData = "scheduler:total:server"
|
||||
SchedulerResetTraffic = "scheduler:reset:traffic"
|
||||
)
|
||||
|
||||
@ -34,6 +34,11 @@ func (m *Service) Start() {
|
||||
if _, err := m.server.Register("@every 180s", totalServerDataTask); err != nil {
|
||||
logger.Errorf("register total server data task failed: %s", err.Error())
|
||||
}
|
||||
// schedule reset traffic task: every 24 hours
|
||||
resetTrafficTask := asynq.NewTask(types.SchedulerResetTraffic, nil)
|
||||
if _, err := m.server.Register("@every 24h", resetTrafficTask); err != nil {
|
||||
logger.Errorf("register reset traffic task failed: %s", err.Error())
|
||||
}
|
||||
|
||||
if err := m.server.Run(); err != nil {
|
||||
logger.Errorf("run scheduler failed: %s", err.Error())
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user