init
This commit is contained in:
commit
e898e6afbe
@ -89,16 +89,20 @@ type (
|
|||||||
CreateRuleGroupRequest {
|
CreateRuleGroupRequest {
|
||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
Icon string `json:"icon"`
|
Icon string `json:"icon"`
|
||||||
|
Type string `json:"type"`
|
||||||
Tags []string `json:"tags"`
|
Tags []string `json:"tags"`
|
||||||
Rules string `json:"rules"`
|
Rules string `json:"rules"`
|
||||||
|
Default bool `json:"default"`
|
||||||
Enable bool `json:"enable"`
|
Enable bool `json:"enable"`
|
||||||
}
|
}
|
||||||
UpdateRuleGroupRequest {
|
UpdateRuleGroupRequest {
|
||||||
Id int64 `json:"id" validate:"required"`
|
Id int64 `json:"id" validate:"required"`
|
||||||
Icon string `json:"icon"`
|
Icon string `json:"icon"`
|
||||||
|
Type string `json:"type"`
|
||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
Tags []string `json:"tags"`
|
Tags []string `json:"tags"`
|
||||||
Rules string `json:"rules"`
|
Rules string `json:"rules"`
|
||||||
|
Default bool `json:"default"`
|
||||||
Enable bool `json:"enable"`
|
Enable bool `json:"enable"`
|
||||||
}
|
}
|
||||||
DeleteRuleGroupRequest {
|
DeleteRuleGroupRequest {
|
||||||
|
|||||||
@ -153,9 +153,10 @@ type (
|
|||||||
NodePushInterval int64 `json:"node_push_interval"`
|
NodePushInterval int64 `json:"node_push_interval"`
|
||||||
}
|
}
|
||||||
InviteConfig {
|
InviteConfig {
|
||||||
ForcedInvite bool `json:"forced_invite"`
|
ForcedInvite bool `json:"forced_invite"`
|
||||||
ReferralPercentage int64 `json:"referral_percentage"`
|
FirstPurchasePercentage int64 `json:"first_purchase_percentage"`
|
||||||
OnlyFirstPurchase bool `json:"only_first_purchase"`
|
FirstYearlyPurchasePercentage int64 `json:"first_yearly_purchase_percentage"`
|
||||||
|
NonFirstPurchasePercentage int64 `json:"non_first_purchase_percentage"`
|
||||||
}
|
}
|
||||||
TelegramConfig {
|
TelegramConfig {
|
||||||
TelegramBotToken string `json:"telegram_bot_token"`
|
TelegramBotToken string `json:"telegram_bot_token"`
|
||||||
@ -515,9 +516,11 @@ type (
|
|||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
Icon string `json:"icon"`
|
Icon string `json:"icon"`
|
||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
|
Type string `json:"type"`
|
||||||
Tags []string `json:"tags"`
|
Tags []string `json:"tags"`
|
||||||
Rules string `json:"rules"`
|
Rules string `json:"rules"`
|
||||||
Enable bool `json:"enable"`
|
Enable bool `json:"enable"`
|
||||||
|
Default bool `json:"default"`
|
||||||
CreatedAt int64 `json:"created_at"`
|
CreatedAt int64 `json:"created_at"`
|
||||||
UpdatedAt int64 `json:"updated_at"`
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
Host: 0.0.0.0
|
Host: 0.0.0.0
|
||||||
Port: 8080
|
Port: 8080
|
||||||
|
<<<<<<< HEAD
|
||||||
TLS:
|
TLS:
|
||||||
Enable: false
|
Enable: false
|
||||||
CertFile: ""
|
CertFile: ""
|
||||||
@ -37,3 +38,41 @@ Redis:
|
|||||||
Host: redis:6379
|
Host: redis:6379
|
||||||
Pass:
|
Pass:
|
||||||
DB: 0
|
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 (
|
require (
|
||||||
|
github.com/Masterminds/sprig/v3 v3.3.0
|
||||||
github.com/fatih/color v1.18.0
|
github.com/fatih/color v1.18.0
|
||||||
github.com/goccy/go-json v0.10.4
|
github.com/goccy/go-json v0.10.4
|
||||||
github.com/golang-migrate/migrate/v4 v4.18.2
|
github.com/golang-migrate/migrate/v4 v4.18.2
|
||||||
@ -66,7 +67,10 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go/compute/metadata v0.6.0 // indirect
|
cloud.google.com/go/compute/metadata v0.6.0 // indirect
|
||||||
|
dario.cat/mergo v1.0.1 // indirect
|
||||||
filippo.io/edwards25519 v1.1.0 // 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/alibabacloud-gateway-spi v0.0.5 // indirect
|
||||||
github.com/alibabacloud-go/debug v1.0.1 // indirect
|
github.com/alibabacloud-go/debug v1.0.1 // indirect
|
||||||
github.com/alibabacloud-go/endpoint-util v1.1.0 // 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/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
github.com/hashicorp/go-multierror v1.1.1 // 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/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // 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/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // 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/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/openzipkin/zipkin-go v0.4.2 // indirect
|
github.com/openzipkin/zipkin-go v0.4.2 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||||
|
github.com/shopspring/decimal v1.4.0 // indirect
|
||||||
github.com/smartwalle/ncrypto v1.0.4 // indirect
|
github.com/smartwalle/ncrypto v1.0.4 // indirect
|
||||||
github.com/smartwalle/ngx v1.0.9 // indirect
|
github.com/smartwalle/ngx v1.0.9 // indirect
|
||||||
github.com/smartwalle/nsign 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 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 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
|
||||||
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
|
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 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
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/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 h1:RDkg3pyE1qGbBpRWmvSN9RNZC5nUrOaEPiEpEb8y2f0=
|
||||||
github.com/GUAIK-ORG/go-snowflake v0.0.0-20200116064823-220c4260e85f/go.mod h1:zA7AF9RTfpluCfz0omI4t5KCMaWHUMicsZoMccnaT44=
|
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 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
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=
|
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/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 h1:+5iIEAyA9K/lcSPvx3qoPtsKJeKI5u9aOIvUmSsazEw=
|
||||||
github.com/hibiken/asynq v0.24.1/go.mod h1:u5qVeSbrnfT+vtG5Mq8ZPzQu/BmCKMHvTGb91uy9Tts=
|
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 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
|
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.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 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
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 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
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=
|
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 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
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/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 h1:i1VwJeu70EmwpsXXz6GZZnMAtRx5MTfn2dPoql/L3zE=
|
||||||
github.com/smartwalle/alipay/v3 v3.2.23/go.mod h1:lVqFiupPf8YsAXaq5JXcwqnOUC2MCF+2/5vub+RlagE=
|
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=
|
github.com/smartwalle/ncrypto v1.0.4 h1:P2rqQxDepJwgeO5ShoC+wGcK2wNJDmcdBOWAksuIgx8=
|
||||||
|
|||||||
@ -17,13 +17,11 @@ func Email(ctx *svc.ServiceContext) {
|
|||||||
logger.Debug("Email config initialization")
|
logger.Debug("Email config initialization")
|
||||||
method, err := ctx.AuthModel.FindOneByMethod(context.Background(), "email")
|
method, err := ctx.AuthModel.FindOneByMethod(context.Background(), "email")
|
||||||
if err != nil {
|
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 cfg config.EmailConfig
|
||||||
var emailConfig = new(auth.EmailAuthConfig)
|
var emailConfig = new(auth.EmailAuthConfig)
|
||||||
if err := emailConfig.Unmarshal(method.Config); err != nil {
|
emailConfig.Unmarshal(method.Config)
|
||||||
panic(fmt.Sprintf("failed to unmarshal email auth config: %v", err.Error()))
|
|
||||||
}
|
|
||||||
tool.DeepCopy(&cfg, emailConfig)
|
tool.DeepCopy(&cfg, emailConfig)
|
||||||
cfg.Enable = *method.Enabled
|
cfg.Enable = *method.Enabled
|
||||||
value, _ := json.Marshal(emailConfig.PlatformConfig)
|
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'),
|
(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'),
|
(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'),
|
(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'),
|
(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'),
|
(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'),
|
(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'),
|
'2025-04-22 14:25:16.640'),
|
||||||
(23, 'invite', 'ForcedInvite', 'false', 'bool', 'Forced invite', '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'),
|
'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'),
|
'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'),
|
'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'),
|
'2025-04-22 14:25:16.640'),
|
||||||
(27, 'register', 'EnableTrial', 'false', 'bool', 'is enable trial', '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'),
|
'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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/perfect-panel/server/pkg/logger"
|
"github.com/perfect-panel/server/pkg/logger"
|
||||||
|
|
||||||
@ -21,9 +20,7 @@ func Mobile(ctx *svc.ServiceContext) {
|
|||||||
}
|
}
|
||||||
var cfg config.MobileConfig
|
var cfg config.MobileConfig
|
||||||
var mobileConfig auth.MobileAuthConfig
|
var mobileConfig auth.MobileAuthConfig
|
||||||
if err := mobileConfig.Unmarshal(method.Config); err != nil {
|
mobileConfig.Unmarshal(method.Config)
|
||||||
panic(fmt.Sprintf("failed to unmarshal mobile auth config: %v", err.Error()))
|
|
||||||
}
|
|
||||||
tool.DeepCopy(&cfg, mobileConfig)
|
tool.DeepCopy(&cfg, mobileConfig)
|
||||||
cfg.Enable = *method.Enabled
|
cfg.Enable = *method.Enabled
|
||||||
value, _ := json.Marshal(mobileConfig.PlatformConfig)
|
value, _ := json.Marshal(mobileConfig.PlatformConfig)
|
||||||
|
|||||||
@ -119,9 +119,10 @@ type File struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type InviteConfig struct {
|
type InviteConfig struct {
|
||||||
ForcedInvite bool `yaml:"ForcedInvite" default:"false"`
|
ForcedInvite bool `yaml:"ForcedInvite" default:"false"`
|
||||||
ReferralPercentage int64 `yaml:"ReferralPercentage" default:"0"`
|
FirstPurchasePercentage int64 `yaml:"FirstPurchasePercentage" default:"20"`
|
||||||
OnlyFirstPurchase bool `yaml:"OnlyFirstPurchase" default:"false"`
|
FirstYearlyPurchasePercentage int64 `yaml:"FirstYearlyPurchasePercentage" default:"25"`
|
||||||
|
NonFirstPurchasePercentage int64 `yaml:"NonFirstPurchasePercentage" default:"10"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Telegram struct {
|
type Telegram struct {
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import (
|
|||||||
"github.com/perfect-panel/server/pkg/result"
|
"github.com/perfect-panel/server/pkg/result"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Create rule group
|
// CreateRuleGroupHandler Create rule group
|
||||||
func CreateRuleGroupHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
func CreateRuleGroupHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
var req types.CreateRuleGroupRequest
|
var req types.CreateRuleGroupRequest
|
||||||
|
|||||||
@ -18,7 +18,7 @@ type GetAuthMethodConfigLogic struct {
|
|||||||
svcCtx *svc.ServiceContext
|
svcCtx *svc.ServiceContext
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get auth method config
|
// NewGetAuthMethodConfigLogic Get auth method config
|
||||||
func NewGetAuthMethodConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAuthMethodConfigLogic {
|
func NewGetAuthMethodConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAuthMethodConfigLogic {
|
||||||
return &GetAuthMethodConfigLogic{
|
return &GetAuthMethodConfigLogic{
|
||||||
Logger: logger.WithContext(ctx),
|
Logger: logger.WithContext(ctx),
|
||||||
|
|||||||
@ -40,34 +40,32 @@ func (l *UpdateAuthMethodConfigLogic) UpdateAuthMethodConfig(req *types.UpdateAu
|
|||||||
|
|
||||||
tool.DeepCopy(method, req)
|
tool.DeepCopy(method, req)
|
||||||
if req.Config != nil {
|
if req.Config != nil {
|
||||||
if value, ok := req.Config.(map[string]interface{}); ok {
|
_, exist := req.Config.(map[string]interface{})
|
||||||
if req.Method == "email" && value["verify_email_template"] == "" {
|
if !exist {
|
||||||
value["verify_email_template"] = email.DefaultEmailVerifyTemplate
|
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 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 == "email" {
|
||||||
|
configs, _ := json.Marshal(req.Config)
|
||||||
|
emailConfig := new(auth.EmailAuthConfig)
|
||||||
|
emailConfig.Unmarshal(string(configs))
|
||||||
|
req.Config = emailConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
bytes, err := json.Marshal(req.Config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "marshal config failed: %v", err.Error())
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "marshal config failed: %v", err.Error())
|
||||||
}
|
}
|
||||||
method.Config = string(bytes)
|
method.Config = string(bytes)
|
||||||
|
} else {
|
||||||
|
// initialize platform config
|
||||||
|
method.Config = initializePlatformConfig(req.Method).(string)
|
||||||
}
|
}
|
||||||
err = l.svcCtx.AuthModel.Update(l.ctx, method)
|
err = l.svcCtx.AuthModel.Update(l.ctx, method)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -124,3 +122,26 @@ func validatePlatformConfig(platform string, cfg map[string]interface{}) (interf
|
|||||||
}
|
}
|
||||||
return config, nil
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
info := &server.RuleGroup{
|
||||||
err = l.svcCtx.ServerModel.InsertRuleGroup(l.ctx, &server.RuleGroup{
|
Name: req.Name,
|
||||||
Name: req.Name,
|
Icon: req.Icon,
|
||||||
Icon: req.Icon,
|
Type: req.Type,
|
||||||
Tags: tool.StringSliceToString(req.Tags),
|
Tags: tool.StringSliceToString(req.Tags),
|
||||||
Rules: strings.Join(rs, "\n"),
|
Rules: strings.Join(rs, "\n"),
|
||||||
Enable: req.Enable,
|
Default: req.Default,
|
||||||
})
|
Enable: req.Enable,
|
||||||
|
}
|
||||||
|
err = l.svcCtx.ServerModel.InsertRuleGroup(l.ctx, info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Errorw("[CreateRuleGroup] Insert Database Error: ", logger.Field("error", err.Error()))
|
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)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,9 +38,11 @@ func (l *GetRuleGroupListLogic) GetRuleGroupList() (resp *types.GetRuleGroupResp
|
|||||||
Id: v.Id,
|
Id: v.Id,
|
||||||
Icon: v.Icon,
|
Icon: v.Icon,
|
||||||
Name: v.Name,
|
Name: v.Name,
|
||||||
|
Type: v.Type,
|
||||||
Tags: strings.Split(v.Tags, ","),
|
Tags: strings.Split(v.Tags, ","),
|
||||||
Rules: v.Rules,
|
Rules: v.Rules,
|
||||||
Enable: v.Enable,
|
Enable: v.Enable,
|
||||||
|
Default: v.Default,
|
||||||
CreatedAt: v.CreatedAt.UnixMilli(),
|
CreatedAt: v.CreatedAt.UnixMilli(),
|
||||||
UpdatedAt: v.UpdatedAt.UnixMilli(),
|
UpdatedAt: v.UpdatedAt.UnixMilli(),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,15 +36,23 @@ func (l *UpdateRuleGroupLogic) UpdateRuleGroup(req *types.UpdateRuleGroupRequest
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = l.svcCtx.ServerModel.UpdateRuleGroup(l.ctx, &server.RuleGroup{
|
err = l.svcCtx.ServerModel.UpdateRuleGroup(l.ctx, &server.RuleGroup{
|
||||||
Id: req.Id,
|
Id: req.Id,
|
||||||
Icon: req.Icon,
|
Icon: req.Icon,
|
||||||
Name: req.Name,
|
Type: req.Type,
|
||||||
Tags: tool.StringSliceToString(req.Tags),
|
Name: req.Name,
|
||||||
Rules: strings.Join(rs, "\n"),
|
Tags: tool.StringSliceToString(req.Tags),
|
||||||
Enable: req.Enable,
|
Rules: strings.Join(rs, "\n"),
|
||||||
|
Default: req.Default,
|
||||||
|
Enable: req.Enable,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), err.Error())
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,6 +44,7 @@ func (l *UpdateUserBasicInfoLogic) UpdateUserBasicInfo(req *types.UpdateUserBasi
|
|||||||
userInfo.Balance = req.Balance
|
userInfo.Balance = req.Balance
|
||||||
userInfo.GiftAmount = req.GiftAmount
|
userInfo.GiftAmount = req.GiftAmount
|
||||||
userInfo.Commission = req.Commission
|
userInfo.Commission = req.Commission
|
||||||
|
// 手动设置 IsAdmin 字段,因为类型不匹配(*bool vs bool)
|
||||||
userInfo.IsAdmin = &req.IsAdmin
|
userInfo.IsAdmin = &req.IsAdmin
|
||||||
userInfo.Enable = &req.Enable
|
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))
|
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())
|
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{
|
err = l.svcCtx.UserModel.UpdateSubscribe(l.ctx, &user.Subscribe{
|
||||||
Id: req.UserSubscribeId,
|
Id: userSub.Id,
|
||||||
UserId: userSub.UserId,
|
UserId: userSub.UserId,
|
||||||
OrderId: userSub.OrderId,
|
OrderId: userSub.OrderId,
|
||||||
SubscribeId: req.SubscribeId,
|
SubscribeId: req.SubscribeId,
|
||||||
|
|||||||
@ -120,10 +120,11 @@ func (l *GetStatLogic) GetStat() (resp *types.GetStatResponse, err error) {
|
|||||||
protocol = append(protocol, p)
|
protocol = append(protocol, p)
|
||||||
}
|
}
|
||||||
resp = &types.GetStatResponse{
|
resp = &types.GetStatResponse{
|
||||||
User: u,
|
User: u,
|
||||||
Node: n,
|
Node: n,
|
||||||
Country: int64(len(country)),
|
Country: int64(len(country)),
|
||||||
Protocol: protocol,
|
Protocol: protocol,
|
||||||
|
OnlineDevice: l.svcCtx.DeviceManager.GetOnlineDeviceCount(),
|
||||||
}
|
}
|
||||||
val, _ := json.Marshal(*resp)
|
val, _ := json.Marshal(*resp)
|
||||||
_ = l.svcCtx.Redis.Set(l.ctx, config.CommonStatCacheKey, string(val), time.Duration(3600)*time.Second).Err()
|
_ = 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
|
return err
|
||||||
}
|
}
|
||||||
deduction := userInfo.GiftAmount + orderInfo.GiftAmount
|
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 {
|
if err != nil {
|
||||||
l.Errorw("[CloseOrder] Refund deduction amount failed",
|
l.Errorw("[CloseOrder] Refund deduction amount failed",
|
||||||
logger.Field("error", err.Error()),
|
logger.Field("error", err.Error()),
|
||||||
|
|||||||
@ -10,5 +10,6 @@ func getDiscount(discounts []types.SubscribeDiscount, inputMonths int64) float64
|
|||||||
finalDiscount = discount.Discount
|
finalDiscount = discount.Discount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return float64(finalDiscount) / float64(100)
|
return float64(finalDiscount) / float64(100)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,7 +24,8 @@ type PreCreateOrderLogic struct {
|
|||||||
svcCtx *svc.ServiceContext
|
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 {
|
func NewPreCreateOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PreCreateOrderLogic {
|
||||||
return &PreCreateOrderLogic{
|
return &PreCreateOrderLogic{
|
||||||
Logger: logger.WithContext(ctx),
|
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) {
|
func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (resp *types.PreOrderResponse, err error) {
|
||||||
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
||||||
if !ok {
|
if !ok {
|
||||||
logger.Error("current user is not found in context")
|
logger.Error("current user is not found in context")
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
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
|
// find subscribe plan
|
||||||
sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, req.SubscribeId)
|
sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, req.SubscribeId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -52,6 +62,7 @@ func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (r
|
|||||||
discount = getDiscount(dis, req.Quantity)
|
discount = getDiscount(dis, req.Quantity)
|
||||||
}
|
}
|
||||||
price := sub.UnitPrice * req.Quantity
|
price := sub.UnitPrice * req.Quantity
|
||||||
|
|
||||||
amount := int64(float64(price) * discount)
|
amount := int64(float64(price) * discount)
|
||||||
discountAmount := price - amount
|
discountAmount := price - amount
|
||||||
var couponAmount int64
|
var couponAmount int64
|
||||||
@ -72,7 +83,7 @@ func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (r
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
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())
|
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 {
|
if req.Payment != 0 {
|
||||||
payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment)
|
payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment)
|
||||||
if err != nil {
|
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())
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find payment method error: %v", err.Error())
|
||||||
}
|
}
|
||||||
// Calculate the handling fee
|
// Calculate the handling fee
|
||||||
|
|||||||
@ -31,7 +31,8 @@ const (
|
|||||||
CloseOrderTimeMinutes = 15
|
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 {
|
func NewPurchaseLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PurchaseLogic {
|
||||||
return &PurchaseLogic{
|
return &PurchaseLogic{
|
||||||
Logger: logger.WithContext(ctx),
|
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) {
|
func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.PurchaseOrderResponse, err error) {
|
||||||
|
|
||||||
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
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")
|
logger.Error("current user is not found in context")
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
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
|
// find user subscription
|
||||||
userSub, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, u.Id)
|
userSub, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, u.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -142,7 +152,7 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
|
|||||||
// find payment method
|
// find payment method
|
||||||
payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment)
|
payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment)
|
||||||
if err != nil {
|
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())
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find payment method error: %v", err.Error())
|
||||||
}
|
}
|
||||||
var feeAmount int64
|
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
|
// update user deduction && Pre deduction ,Return after canceling the order
|
||||||
if orderInfo.GiftAmount > 0 {
|
if orderInfo.GiftAmount > 0 {
|
||||||
// update user deduction && Pre deduction ,Return after canceling the order
|
// update user deduction && Pre deduction ,Return after canceling the order
|
||||||
if e := l.svcCtx.UserModel.Update(l.ctx, u, db); err != nil {
|
if e := l.svcCtx.UserModel.Update(l.ctx, u, db); e != nil {
|
||||||
l.Errorw("[Purchase] Database update error", logger.Field("error", err.Error()), logger.Field("user", u))
|
l.Errorw("[Purchase] Database update error", logger.Field("error", e.Error()), logger.Field("user", u))
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
// create deduction record
|
// 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 {
|
if e := db.Model(&user.GiftAmountLog{}).Create(&giftAmountLog).Error; e != nil {
|
||||||
l.Errorw("[Purchase] Database insert error",
|
l.Errorw("[Purchase] Database insert error",
|
||||||
logger.Field("error", err.Error()),
|
logger.Field("error", e.Error()),
|
||||||
logger.Field("deductionLog", giftAmountLog),
|
logger.Field("deductionLog", giftAmountLog),
|
||||||
)
|
)
|
||||||
return e
|
return e
|
||||||
@ -213,14 +223,14 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
|
|||||||
}
|
}
|
||||||
val, err := json.Marshal(payload)
|
val, err := json.Marshal(payload)
|
||||||
if err != nil {
|
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))
|
task := asynq.NewTask(queue.DeferCloseOrder, val, asynq.MaxRetry(3))
|
||||||
taskInfo, err := l.svcCtx.Queue.Enqueue(task, asynq.ProcessIn(CloseOrderTimeMinutes*time.Minute))
|
taskInfo, err := l.svcCtx.Queue.Enqueue(task, asynq.ProcessIn(CloseOrderTimeMinutes*time.Minute))
|
||||||
if err != nil {
|
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 {
|
} 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{
|
return &types.PurchaseOrderResponse{
|
||||||
|
|||||||
@ -27,7 +27,7 @@ type RenewalLogic struct {
|
|||||||
svcCtx *svc.ServiceContext
|
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 {
|
func NewRenewalLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RenewalLogic {
|
||||||
return &RenewalLogic{
|
return &RenewalLogic{
|
||||||
Logger: logger.WithContext(ctx),
|
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) {
|
func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.RenewalOrderResponse, err error) {
|
||||||
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
||||||
if !ok {
|
if !ok {
|
||||||
logger.Error("current user is not found in context")
|
logger.Error("current user is not found in context")
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
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()
|
orderNo := tool.GenerateTradeNo()
|
||||||
// find user subscribe
|
// find user subscribe
|
||||||
userSubscribe, err := l.svcCtx.UserModel.FindOneUserSubscribe(l.ctx, req.UserSubscribeID)
|
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)
|
payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Errorw("[Renewal] Database query error", logger.Field("error", err.Error()), logger.Field("payment", req.Payment))
|
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
|
amount -= coupon
|
||||||
|
|
||||||
@ -109,8 +116,8 @@ func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.Rene
|
|||||||
if u.GiftAmount > 0 {
|
if u.GiftAmount > 0 {
|
||||||
if u.GiftAmount >= amount {
|
if u.GiftAmount >= amount {
|
||||||
deductionAmount = amount
|
deductionAmount = amount
|
||||||
|
u.GiftAmount -= deductionAmount
|
||||||
amount = 0
|
amount = 0
|
||||||
u.GiftAmount -= amount
|
|
||||||
} else {
|
} else {
|
||||||
deductionAmount = u.GiftAmount
|
deductionAmount = u.GiftAmount
|
||||||
amount -= u.GiftAmount
|
amount -= u.GiftAmount
|
||||||
@ -152,7 +159,7 @@ func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.Rene
|
|||||||
if orderInfo.GiftAmount > 0 {
|
if orderInfo.GiftAmount > 0 {
|
||||||
// update user deduction && Pre deduction ,Return after canceling the order
|
// update user deduction && Pre deduction ,Return after canceling the order
|
||||||
if err := l.svcCtx.UserModel.Update(l.ctx, u, db); err != nil {
|
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
|
return err
|
||||||
}
|
}
|
||||||
// create deduction record
|
// create deduction record
|
||||||
|
|||||||
@ -43,10 +43,26 @@ func (l *GetSubscriptionLogic) GetSubscription() (resp *types.GetSubscriptionRes
|
|||||||
tool.DeepCopy(&sub, item)
|
tool.DeepCopy(&sub, item)
|
||||||
if item.Discount != "" {
|
if item.Discount != "" {
|
||||||
var discount []types.SubscribeDiscount
|
var discount []types.SubscribeDiscount
|
||||||
|
|
||||||
_ = json.Unmarshal([]byte(item.Discount), &discount)
|
_ = json.Unmarshal([]byte(item.Discount), &discount)
|
||||||
sub.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
|
list[i] = sub
|
||||||
}
|
}
|
||||||
resp.List = list
|
resp.List = list
|
||||||
|
|||||||
@ -122,6 +122,7 @@ func (l *PurchaseCheckoutLogic) PurchaseCheckout(req *types.CheckoutOrderRequest
|
|||||||
if err = l.balancePayment(userInfo, orderInfo); err != nil {
|
if err = l.balancePayment(userInfo, orderInfo); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp = &types.CheckoutOrderResponse{
|
resp = &types.CheckoutOrderResponse{
|
||||||
Type: "balance", // Payment completed immediately
|
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
|
// balancePayment processes balance payment with gift amount priority logic
|
||||||
// It prioritizes using gift amount first, then regular balance, and creates proper audit logs
|
// 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 {
|
func (l *PurchaseCheckoutLogic) balancePayment(u *user.User, o *order.Order) error {
|
||||||
|
var userInfo user.User
|
||||||
|
var err error
|
||||||
if o.Amount == 0 {
|
if o.Amount == 0 {
|
||||||
// No payment required for zero-amount orders
|
// 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
|
// Retrieve latest user information with row-level locking
|
||||||
err := db.Model(&user.User{}).Where("id = ?", u.Id).First(&userInfo).Error
|
err := db.Model(&user.User{}).Where("id = ?", u.Id).First(&userInfo).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -420,6 +435,7 @@ func (l *PurchaseCheckoutLogic) balancePayment(u *user.User, o *order.Order) err
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
activation:
|
||||||
// Enqueue order activation task for immediate processing
|
// Enqueue order activation task for immediate processing
|
||||||
payload := queueType.ForthwithActivateOrderPayload{
|
payload := queueType.ForthwithActivateOrderPayload{
|
||||||
OrderNo: o.OrderNo,
|
OrderNo: o.OrderNo,
|
||||||
|
|||||||
@ -40,6 +40,7 @@ func (l *QuerySubscribeListLogic) QuerySubscribeList() (resp *types.QuerySubscri
|
|||||||
list := make([]types.Subscribe, len(data))
|
list := make([]types.Subscribe, len(data))
|
||||||
for i, item := range data {
|
for i, item := range data {
|
||||||
var sub types.Subscribe
|
var sub types.Subscribe
|
||||||
|
|
||||||
tool.DeepCopy(&sub, item)
|
tool.DeepCopy(&sub, item)
|
||||||
if item.Discount != "" {
|
if item.Discount != "" {
|
||||||
var discount []types.SubscribeDiscount
|
var discount []types.SubscribeDiscount
|
||||||
@ -48,6 +49,15 @@ func (l *QuerySubscribeListLogic) QuerySubscribeList() (resp *types.QuerySubscri
|
|||||||
list[i] = sub
|
list[i] = sub
|
||||||
}
|
}
|
||||||
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
|
resp.List = list
|
||||||
return
|
return
|
||||||
|
|||||||
@ -52,8 +52,10 @@ func (l *BindOAuthCallbackLogic) BindOAuthCallback(req *types.BindOAuthCallbackR
|
|||||||
err = l.google(req)
|
err = l.google(req)
|
||||||
case "apple":
|
case "apple":
|
||||||
err = l.apple(req)
|
err = l.apple(req)
|
||||||
|
case "telegram":
|
||||||
|
err = l.telegram(req)
|
||||||
default:
|
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)
|
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "oauth login method not support: %v", req.Method)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -212,3 +214,7 @@ func (l *BindOAuthCallbackLogic) apple(req *types.BindOAuthCallbackRequest) erro
|
|||||||
}
|
}
|
||||||
return nil
|
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)
|
sub.ResetTime = calculateNextResetTime(&sub)
|
||||||
resp.List = append(resp.List, sub)
|
resp.List = append(resp.List, sub)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -48,7 +48,7 @@ func (l *UpdateBindEmailLogic) UpdateBindEmail(req *types.UpdateBindEmailRequest
|
|||||||
if m.Id > 0 {
|
if m.Id > 0 {
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.UserExist), "email already bind")
|
return errors.Wrapf(xerr.NewErrCode(xerr.UserExist), "email already bind")
|
||||||
}
|
}
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if method.Id == 0 {
|
||||||
method = &user.AuthMethods{
|
method = &user.AuthMethods{
|
||||||
UserId: u.Id,
|
UserId: u.Id,
|
||||||
AuthType: "email",
|
AuthType: "email",
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/perfect-panel/server/pkg/adapter"
|
"github.com/perfect-panel/server/pkg/adapter"
|
||||||
"github.com/perfect-panel/server/pkg/adapter/shadowrocket"
|
"github.com/perfect-panel/server/pkg/adapter/shadowrocket"
|
||||||
"github.com/perfect-panel/server/pkg/adapter/surfboard"
|
"github.com/perfect-panel/server/pkg/adapter/surfboard"
|
||||||
|
"github.com/perfect-panel/server/pkg/adapter/surge"
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/model/server"
|
"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)
|
serverIds := tool.StringToInt64Slice(subDetails.Server)
|
||||||
groupIds := tool.StringToInt64Slice(subDetails.ServerGroup)
|
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)
|
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)
|
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))
|
l.Debugf("[Query Subscribe]found servers: %v", len(servers))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -235,8 +273,9 @@ func (l *SubscribeLogic) buildClientConfig(req *types.SubscribeRequest, userSub
|
|||||||
})
|
})
|
||||||
case "loon":
|
case "loon":
|
||||||
resp = proxyManager.BuildLoon(userSub.UUID)
|
resp = proxyManager.BuildLoon(userSub.UUID)
|
||||||
|
l.setLoonHeaders()
|
||||||
case "surfboard":
|
case "surfboard":
|
||||||
subsURL := l.getSubscribeURL(userSub.Token)
|
subsURL := l.getSubscribeURL(userSub.Token, "surfboard")
|
||||||
resp = proxyManager.BuildSurfboard(l.svc.Config.Site.SiteName, surfboard.UserInfo{
|
resp = proxyManager.BuildSurfboard(l.svc.Config.Site.SiteName, surfboard.UserInfo{
|
||||||
Upload: userSub.Upload,
|
Upload: userSub.Upload,
|
||||||
Download: userSub.Download,
|
Download: userSub.Download,
|
||||||
@ -248,7 +287,17 @@ func (l *SubscribeLogic) buildClientConfig(req *types.SubscribeRequest, userSub
|
|||||||
l.setSurfboardHeaders()
|
l.setSurfboardHeaders()
|
||||||
case "v2rayn":
|
case "v2rayn":
|
||||||
resp = proxyManager.BuildV2rayN(userSub.UUID)
|
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:
|
default:
|
||||||
resp = proxyManager.BuildGeneral(userSub.UUID)
|
resp = proxyManager.BuildGeneral(userSub.UUID)
|
||||||
}
|
}
|
||||||
@ -270,14 +319,24 @@ func (l *SubscribeLogic) setSurfboardHeaders() {
|
|||||||
l.ctx.Header("Content-Type", "application/octet-stream; charset=UTF-8")
|
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 {
|
if l.svc.Config.Subscribe.PanDomain {
|
||||||
return fmt.Sprintf("https://%s", l.ctx.Request.Host)
|
return fmt.Sprintf("https://%s", l.ctx.Request.Host)
|
||||||
}
|
}
|
||||||
|
|
||||||
if l.svc.Config.Subscribe.SubscribeDomain != "" {
|
if l.svc.Config.Subscribe.SubscribeDomain != "" {
|
||||||
domains := strings.Split(l.svc.Config.Subscribe.SubscribeDomain, "\n")
|
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)
|
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 (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/perfect-panel/server/pkg/email"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Auth struct {
|
type Auth struct {
|
||||||
@ -124,15 +126,55 @@ type EmailAuthConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *EmailAuthConfig) Marshal() string {
|
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)
|
bytes, err := json.Marshal(l)
|
||||||
if err != nil {
|
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)
|
return string(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *EmailAuthConfig) Unmarshal(data string) error {
|
func (l *EmailAuthConfig) Unmarshal(data string) {
|
||||||
return json.Unmarshal([]byte(data), &l)
|
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
|
// SMTPConfig Email SMTP configuration
|
||||||
@ -167,13 +209,28 @@ type MobileAuthConfig struct {
|
|||||||
func (l *MobileAuthConfig) Marshal() string {
|
func (l *MobileAuthConfig) Marshal() string {
|
||||||
bytes, err := json.Marshal(l)
|
bytes, err := json.Marshal(l)
|
||||||
if err != nil {
|
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)
|
return string(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *MobileAuthConfig) Unmarshal(data string) error {
|
func (l *MobileAuthConfig) Unmarshal(data string) {
|
||||||
return json.Unmarshal([]byte(data), &l)
|
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 {
|
type AlibabaCloudConfig struct {
|
||||||
|
|||||||
@ -32,6 +32,8 @@ type customServerLogicModel interface {
|
|||||||
QueryAllRuleGroup(ctx context.Context) ([]*RuleGroup, error)
|
QueryAllRuleGroup(ctx context.Context) ([]*RuleGroup, error)
|
||||||
FindServersByTag(ctx context.Context, tag string) ([]*Server, error)
|
FindServersByTag(ctx context.Context, tag string) ([]*Server, error)
|
||||||
FindServerTags(ctx context.Context) ([]string, error)
|
FindServerTags(ctx context.Context) ([]string, error)
|
||||||
|
|
||||||
|
SetDefaultRuleGroup(ctx context.Context, id int64) error
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -275,3 +277,16 @@ func (m *customServerModel) FindServersByTag(ctx context.Context, tag string) ([
|
|||||||
})
|
})
|
||||||
return data, err
|
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))
|
||||||
|
}
|
||||||
|
|||||||
@ -9,9 +9,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
RelayModeNone = "none"
|
RelayModeNone = "none"
|
||||||
RelayModeAll = "all"
|
RelayModeAll = "all"
|
||||||
RelayModeRandom = "random"
|
RelayModeRandom = "random"
|
||||||
|
RuleGroupTypeReject = "reject"
|
||||||
|
RuleGroupTypeDefault = "default"
|
||||||
|
RuleGroupTypeDirect = "direct"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ServerFilter struct {
|
type ServerFilter struct {
|
||||||
@ -178,9 +181,11 @@ type RuleGroup struct {
|
|||||||
Id int64 `gorm:"primary_key"`
|
Id int64 `gorm:"primary_key"`
|
||||||
Icon string `gorm:"type:MEDIUMTEXT;comment:Rule Group Icon"`
|
Icon string `gorm:"type:MEDIUMTEXT;comment:Rule Group Icon"`
|
||||||
Name string `gorm:"type:varchar(100);not null;default:'';comment:Rule Group Name"`
|
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"`
|
Tags string `gorm:"type:text;comment:Selected Node Tags"`
|
||||||
Rules string `gorm:"type:MEDIUMTEXT;comment:Rules"`
|
Rules string `gorm:"type:MEDIUMTEXT;comment:Rules"`
|
||||||
Enable bool `gorm:"type:tinyint(1);not null;default:1;comment:Rule Group Enable"`
|
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"`
|
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
|
||||||
UpdatedAt time.Time `gorm:"comment:Update 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)).
|
return conn.Where("`expire_time` > ? OR `finished_at` >= ? OR `expire_time` = ?", now, sevenDaysAgo, time.UnixMilli(0)).
|
||||||
Preload("Subscribe").
|
Preload("Subscribe").
|
||||||
|
Order("created_at DESC").
|
||||||
Find(&list).Error
|
Find(&list).Error
|
||||||
})
|
})
|
||||||
return list, err
|
return list, err
|
||||||
|
|||||||
@ -32,6 +32,11 @@ type Announcement struct {
|
|||||||
UpdatedAt int64 `json:"updated_at"`
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AnyTLS struct {
|
||||||
|
Port int `json:"port" validate:"required"`
|
||||||
|
SecurityConfig SecurityConfig `json:"security_config"`
|
||||||
|
}
|
||||||
|
|
||||||
type AppAuthCheckRequest struct {
|
type AppAuthCheckRequest struct {
|
||||||
Method string `json:"method" validate:"required" validate:"required,oneof=device email mobile"`
|
Method string `json:"method" validate:"required" validate:"required,oneof=device email mobile"`
|
||||||
Account string `json:"account"`
|
Account string `json:"account"`
|
||||||
@ -441,11 +446,13 @@ type CreatePaymentMethodRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CreateRuleGroupRequest struct {
|
type CreateRuleGroupRequest struct {
|
||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
Icon string `json:"icon"`
|
Icon string `json:"icon"`
|
||||||
Tags []string `json:"tags"`
|
Type string `json:"type"`
|
||||||
Rules string `json:"rules"`
|
Tags []string `json:"tags"`
|
||||||
Enable bool `json:"enable"`
|
Rules string `json:"rules"`
|
||||||
|
Default bool `json:"default"`
|
||||||
|
Enable bool `json:"enable"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateSubscribeGroupRequest struct {
|
type CreateSubscribeGroupRequest struct {
|
||||||
@ -468,6 +475,7 @@ type CreateSubscribeRequest struct {
|
|||||||
GroupId int64 `json:"group_id"`
|
GroupId int64 `json:"group_id"`
|
||||||
ServerGroup []int64 `json:"server_group"`
|
ServerGroup []int64 `json:"server_group"`
|
||||||
Server []int64 `json:"server"`
|
Server []int64 `json:"server"`
|
||||||
|
ServerCount int64 `json:"server_count"`
|
||||||
Show *bool `json:"show"`
|
Show *bool `json:"show"`
|
||||||
Sell *bool `json:"sell"`
|
Sell *bool `json:"sell"`
|
||||||
DeductionRatio int64 `json:"deduction_ratio"`
|
DeductionRatio int64 `json:"deduction_ratio"`
|
||||||
@ -854,10 +862,11 @@ type GetServerUserListResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type GetStatResponse struct {
|
type GetStatResponse struct {
|
||||||
User int64 `json:"user"`
|
User int64 `json:"user"`
|
||||||
Node int64 `json:"node"`
|
Node int64 `json:"node"`
|
||||||
Country int64 `json:"country"`
|
Country int64 `json:"country"`
|
||||||
Protocol []string `json:"protocol"`
|
Protocol []string `json:"protocol"`
|
||||||
|
OnlineDevice int64 `json:"online_device"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetSubscribeDetailsRequest struct {
|
type GetSubscribeDetailsRequest struct {
|
||||||
@ -1037,9 +1046,10 @@ type Hysteria2 struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type InviteConfig struct {
|
type InviteConfig struct {
|
||||||
ForcedInvite bool `json:"forced_invite"`
|
ForcedInvite bool `json:"forced_invite"`
|
||||||
ReferralPercentage int64 `json:"referral_percentage"`
|
FirstPurchasePercentage int64 `json:"first_purchase_percentage"`
|
||||||
OnlyFirstPurchase bool `json:"only_first_purchase"`
|
FirstYearlyPurchasePercentage int64 `json:"first_yearly_purchase_percentage"`
|
||||||
|
NonFirstPurchasePercentage int64 `json:"non_first_purchase_percentage"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type KickOfflineRequest struct {
|
type KickOfflineRequest struct {
|
||||||
@ -1553,9 +1563,11 @@ type ServerRuleGroup struct {
|
|||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
Icon string `json:"icon"`
|
Icon string `json:"icon"`
|
||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
|
Type string `json:"type"`
|
||||||
Tags []string `json:"tags"`
|
Tags []string `json:"tags"`
|
||||||
Rules string `json:"rules"`
|
Rules string `json:"rules"`
|
||||||
Enable bool `json:"enable"`
|
Enable bool `json:"enable"`
|
||||||
|
Default bool `json:"default"`
|
||||||
CreatedAt int64 `json:"created_at"`
|
CreatedAt int64 `json:"created_at"`
|
||||||
UpdatedAt int64 `json:"updated_at"`
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
}
|
}
|
||||||
@ -1647,6 +1659,7 @@ type Subscribe struct {
|
|||||||
GroupId int64 `json:"group_id"`
|
GroupId int64 `json:"group_id"`
|
||||||
ServerGroup []int64 `json:"server_group"`
|
ServerGroup []int64 `json:"server_group"`
|
||||||
Server []int64 `json:"server"`
|
Server []int64 `json:"server"`
|
||||||
|
ServerCount int64 `json:"server_count"`
|
||||||
Show bool `json:"show"`
|
Show bool `json:"show"`
|
||||||
Sell bool `json:"sell"`
|
Sell bool `json:"sell"`
|
||||||
Sort int64 `json:"sort"`
|
Sort int64 `json:"sort"`
|
||||||
@ -1955,12 +1968,14 @@ type UpdatePaymentMethodRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UpdateRuleGroupRequest struct {
|
type UpdateRuleGroupRequest struct {
|
||||||
Id int64 `json:"id" validate:"required"`
|
Id int64 `json:"id" validate:"required"`
|
||||||
Icon string `json:"icon"`
|
Icon string `json:"icon"`
|
||||||
Name string `json:"name" validate:"required"`
|
Type string `json:"type"`
|
||||||
Tags []string `json:"tags"`
|
Name string `json:"name" validate:"required"`
|
||||||
Rules string `json:"rules"`
|
Tags []string `json:"tags"`
|
||||||
Enable bool `json:"enable"`
|
Rules string `json:"rules"`
|
||||||
|
Default bool `json:"default"`
|
||||||
|
Enable bool `json:"enable"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateSubscribeGroupRequest struct {
|
type UpdateSubscribeGroupRequest struct {
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
package adapter
|
package adapter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"embed"
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/model/server"
|
"github.com/perfect-panel/server/internal/model/server"
|
||||||
"github.com/perfect-panel/server/pkg/adapter/clash"
|
"github.com/perfect-panel/server/pkg/adapter/clash"
|
||||||
"github.com/perfect-panel/server/pkg/adapter/general"
|
"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/shadowrocket"
|
||||||
"github.com/perfect-panel/server/pkg/adapter/singbox"
|
"github.com/perfect-panel/server/pkg/adapter/singbox"
|
||||||
"github.com/perfect-panel/server/pkg/adapter/surfboard"
|
"github.com/perfect-panel/server/pkg/adapter/surfboard"
|
||||||
|
"github.com/perfect-panel/server/pkg/adapter/surge"
|
||||||
"github.com/perfect-panel/server/pkg/adapter/v2rayn"
|
"github.com/perfect-panel/server/pkg/adapter/v2rayn"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//go:embed template/*
|
||||||
|
var TemplateFS embed.FS
|
||||||
|
|
||||||
|
var (
|
||||||
|
AutoSelect = "Auto - UrlTest"
|
||||||
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Nodes []*server.Server
|
Nodes []*server.Server
|
||||||
Rules []*server.RuleGroup
|
Rules []*server.RuleGroup
|
||||||
@ -25,62 +35,68 @@ type Adapter struct {
|
|||||||
|
|
||||||
func NewAdapter(cfg *Config) *Adapter {
|
func NewAdapter(cfg *Config) *Adapter {
|
||||||
// 转换服务器列表
|
// 转换服务器列表
|
||||||
proxies := adapterProxies(cfg.Nodes)
|
proxies, nodes, tags := adapterProxies(cfg.Nodes)
|
||||||
// 生成代理组
|
|
||||||
proxyGroup, region := generateProxyGroup(proxies)
|
|
||||||
|
|
||||||
// 转换规则组
|
// 转换规则组
|
||||||
g, r := adapterRules(cfg.Rules)
|
g, r, d := adapterRules(cfg.Rules)
|
||||||
|
if d == "" {
|
||||||
// 加入兜底节点
|
d = AutoSelect
|
||||||
for i, group := range g {
|
|
||||||
if len(group.Proxies) == 0 {
|
|
||||||
g[i].Proxies = append([]string{"DIRECT"}, region...)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// 生成默认代理组
|
||||||
|
proxyGroup := append(generateDefaultGroup(), g...)
|
||||||
// 合并代理组
|
// 合并代理组
|
||||||
proxyGroup = RemoveEmptyGroup(append(proxyGroup, g...))
|
proxyGroup = SortGroups(proxyGroup, nodes, tags, d)
|
||||||
// 处理标签
|
|
||||||
proxyGroup = adapterTags(cfg.Tags, proxyGroup)
|
|
||||||
return &Adapter{
|
return &Adapter{
|
||||||
Adapter: proxy.Adapter{
|
Adapter: proxy.Adapter{
|
||||||
Proxies: proxies,
|
Proxies: proxies,
|
||||||
Group: proxyGroup,
|
Group: proxyGroup,
|
||||||
Rules: r,
|
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) {
|
func (m *Adapter) BuildClash(uuid string) ([]byte, error) {
|
||||||
client := clash.NewClash(m.Adapter)
|
client := clash.NewClash(m.Adapter)
|
||||||
return client.Build(uuid)
|
return client.Build(uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BuildGeneral generates a general configuration for the given UUID.
|
||||||
func (m *Adapter) BuildGeneral(uuid string) []byte {
|
func (m *Adapter) BuildGeneral(uuid string) []byte {
|
||||||
return general.GenerateBase64General(m.Proxies, uuid)
|
return general.GenerateBase64General(m.Proxies, uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BuildLoon generates a Loon configuration for the given UUID.
|
||||||
func (m *Adapter) BuildLoon(uuid string) []byte {
|
func (m *Adapter) BuildLoon(uuid string) []byte {
|
||||||
return loon.BuildLoon(m.Proxies, uuid)
|
return loon.BuildLoon(m.Proxies, uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BuildQuantumultX generates a Quantumult X configuration for the given UUID.
|
||||||
func (m *Adapter) BuildQuantumultX(uuid string) string {
|
func (m *Adapter) BuildQuantumultX(uuid string) string {
|
||||||
return quantumultx.BuildQuantumultX(m.Proxies, uuid)
|
return quantumultx.BuildQuantumultX(m.Proxies, uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BuildSingbox generates a Singbox configuration for the given UUID.
|
||||||
func (m *Adapter) BuildSingbox(uuid string) ([]byte, error) {
|
func (m *Adapter) BuildSingbox(uuid string) ([]byte, error) {
|
||||||
return singbox.BuildSingbox(m.Adapter, uuid)
|
return singbox.BuildSingbox(m.Adapter, uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Adapter) BuildShadowrocket(uuid string, userInfo shadowrocket.UserInfo) []byte {
|
func (m *Adapter) BuildShadowrocket(uuid string, userInfo shadowrocket.UserInfo) []byte {
|
||||||
return shadowrocket.BuildShadowrocket(m.Proxies, uuid, userInfo)
|
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 {
|
func (m *Adapter) BuildSurfboard(siteName string, user surfboard.UserInfo) []byte {
|
||||||
return surfboard.BuildSurfboard(m.Adapter, siteName, user)
|
return surfboard.BuildSurfboard(m.Adapter, siteName, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BuildV2rayN generates a V2rayN configuration for the given UUID.
|
||||||
func (m *Adapter) BuildV2rayN(uuid string) []byte {
|
func (m *Adapter) BuildV2rayN(uuid string) []byte {
|
||||||
return v2rayn.NewV2rayN(m.Adapter).Build(uuid)
|
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
|
package clash
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/Masterminds/sprig/v3"
|
||||||
"github.com/perfect-panel/server/pkg/adapter/proxy"
|
"github.com/perfect-panel/server/pkg/adapter/proxy"
|
||||||
"github.com/perfect-panel/server/pkg/logger"
|
"github.com/perfect-panel/server/pkg/logger"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
@ -20,22 +23,16 @@ func NewClash(adapter proxy.Adapter) *Clash {
|
|||||||
|
|
||||||
func (c *Clash) Build(uuid string) ([]byte, error) {
|
func (c *Clash) Build(uuid string) ([]byte, error) {
|
||||||
var proxies []Proxy
|
var proxies []Proxy
|
||||||
for _, v := range c.Proxies {
|
for _, proxied := range c.Adapter.Proxies {
|
||||||
p, err := c.parseProxy(v, uuid)
|
p, err := c.parseProxy(proxied, uuid)
|
||||||
if err != nil {
|
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
|
continue
|
||||||
}
|
}
|
||||||
proxies = append(proxies, *p)
|
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
|
var groups []ProxyGroup
|
||||||
for _, group := range c.Group {
|
for _, group := range c.Adapter.Group {
|
||||||
groups = append(groups, ProxyGroup{
|
groups = append(groups, ProxyGroup{
|
||||||
Name: group.Name,
|
Name: group.Name,
|
||||||
Type: string(group.Type),
|
Type: string(group.Type),
|
||||||
@ -44,9 +41,38 @@ func (c *Clash) Build(uuid string) ([]byte, error) {
|
|||||||
Interval: group.Interval,
|
Interval: group.Interval,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
rawConfig.ProxyGroups = groups
|
var rules = append(c.Rules, fmt.Sprintf("MATCH,%s", c.Default))
|
||||||
rawConfig.Rules = append(c.Rules, "MATCH,手动选择")
|
|
||||||
return yaml.Marshal(&rawConfig)
|
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) {
|
func (c *Clash) parseProxy(p proxy.Proxy, uuid string) (*Proxy, error) {
|
||||||
|
|||||||
@ -1,31 +1,50 @@
|
|||||||
package clash
|
package clash
|
||||||
|
|
||||||
const DefaultTemplate = `
|
const DefaultTemplate = `
|
||||||
mixed-port: 7890
|
mode: rule
|
||||||
|
ipv6: true
|
||||||
allow-lan: true
|
allow-lan: true
|
||||||
bind-address: "*"
|
bind-address: "*"
|
||||||
mode: rule
|
mixed-port: 7890
|
||||||
log-level: info
|
log-level: error
|
||||||
external-controller: 127.0.0.1:9090
|
|
||||||
global-client-fingerprint: chrome
|
|
||||||
unified-delay: true
|
unified-delay: true
|
||||||
geox-url:
|
tcp-concurrent: true
|
||||||
mmdb: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.metadb"
|
external-controller: 0.0.0.0:9090
|
||||||
|
|
||||||
|
tun:
|
||||||
|
enable: true
|
||||||
|
stack: system
|
||||||
|
auto-route: true
|
||||||
|
|
||||||
dns:
|
dns:
|
||||||
enable: true
|
enable: true
|
||||||
|
cache-algorithm: arc
|
||||||
|
listen: 0.0.0.0:1053
|
||||||
ipv6: true
|
ipv6: true
|
||||||
enhanced-mode: fake-ip
|
enhanced-mode: fake-ip
|
||||||
fake-ip-range: 198.18.0.1/16
|
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:
|
default-nameserver:
|
||||||
- 120.53.53.53
|
- 119.29.29.29
|
||||||
- 1.12.12.12
|
- 223.5.5.5
|
||||||
nameserver:
|
nameserver:
|
||||||
- https://120.53.53.53/dns-query#skip-cert-verify=true
|
- system
|
||||||
- tls://1.12.12.12#skip-cert-verify=true
|
- 119.29.29.29
|
||||||
proxy-server-nameserver:
|
- 223.5.5.5
|
||||||
- https://120.53.53.53/dns-query#skip-cert-verify=true
|
fallback:
|
||||||
- tls://1.12.12.12#skip-cert-verify=true
|
- 8.8.8.8
|
||||||
|
- 1.1.1.1
|
||||||
|
fallback-filter:
|
||||||
|
geoip: true
|
||||||
|
geoip-code: CN
|
||||||
|
|
||||||
proxies:
|
proxies:
|
||||||
|
|
||||||
|
|||||||
@ -63,6 +63,8 @@ func buildProxy(data proxy.Proxy, uuid string) string {
|
|||||||
return Hysteria2Uri(data, uuid)
|
return Hysteria2Uri(data, uuid)
|
||||||
case "tuic":
|
case "tuic":
|
||||||
return TuicUri(data, uuid)
|
return TuicUri(data, uuid)
|
||||||
|
case "anytls":
|
||||||
|
return AnyTLSUri(data, uuid)
|
||||||
default:
|
default:
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@ -271,6 +273,36 @@ func TuicUri(data proxy.Proxy, uuid string) string {
|
|||||||
return u.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) {
|
func setQuery(q *url.Values, k, v string) {
|
||||||
if v != "" {
|
if v != "" {
|
||||||
q.Set(k, v)
|
q.Set(k, v)
|
||||||
|
|||||||
@ -1,27 +1,61 @@
|
|||||||
package loon
|
package loon
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"embed"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
"github.com/perfect-panel/server/pkg/adapter/proxy"
|
"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 {
|
func BuildLoon(servers []proxy.Proxy, uuid string) []byte {
|
||||||
uri := ""
|
uri := ""
|
||||||
|
nodes := make([]string, 0)
|
||||||
for _, s := range servers {
|
for _, s := range servers {
|
||||||
switch s.Protocol {
|
switch s.Protocol {
|
||||||
case "vmess":
|
case "vmess":
|
||||||
|
nodes = append(nodes, s.Name)
|
||||||
uri += buildVMess(s, uuid)
|
uri += buildVMess(s, uuid)
|
||||||
case "shadowsocks":
|
case "shadowsocks":
|
||||||
|
nodes = append(nodes, s.Name)
|
||||||
uri += buildShadowsocks(s, uuid)
|
uri += buildShadowsocks(s, uuid)
|
||||||
case "trojan":
|
case "trojan":
|
||||||
|
nodes = append(nodes, s.Name)
|
||||||
uri += buildTrojan(s, uuid)
|
uri += buildTrojan(s, uuid)
|
||||||
case "vless":
|
case "vless":
|
||||||
|
nodes = append(nodes, s.Name)
|
||||||
uri += buildVless(s, uuid)
|
uri += buildVless(s, uuid)
|
||||||
case "hysteria2":
|
case "hysteria2":
|
||||||
|
nodes = append(nodes, s.Name)
|
||||||
uri += buildHysteria2(s, uuid)
|
uri += buildHysteria2(s, uuid)
|
||||||
default:
|
default:
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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 []byte(uri)
|
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
|
package proxy
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
// Adapter represents a proxy adapter
|
// Adapter represents a proxy adapter
|
||||||
type Adapter struct {
|
type Adapter struct {
|
||||||
Proxies []Proxy
|
Proxies []Proxy
|
||||||
Group []Group
|
Group []Group
|
||||||
Rules []string
|
Rules []string // rule
|
||||||
Region []string
|
Nodes []string // all node
|
||||||
|
Default string // Default Node
|
||||||
|
TemplateFS *embed.FS // Template file system
|
||||||
}
|
}
|
||||||
|
|
||||||
// Proxy represents a proxy server
|
// Proxy represents a proxy server
|
||||||
type Proxy struct {
|
type Proxy struct {
|
||||||
Name string
|
Name string // Name of the proxy
|
||||||
Server string
|
Server string // Server address of the proxy
|
||||||
Port int
|
Port int // Port of the proxy server
|
||||||
Protocol string
|
Protocol string // Protocol type (e.g., shadowsocks, vless, vmess, trojan, hysteria2, tuic, anytls)
|
||||||
Country string
|
Country string // Country of the proxy
|
||||||
Option any
|
Tags []string // Tags for the proxy
|
||||||
|
Option any // Additional options for the proxy configuration
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group represents a group of proxies
|
// Group represents a group of proxies
|
||||||
@ -25,6 +30,10 @@ type Group struct {
|
|||||||
Proxies []string
|
Proxies []string
|
||||||
URL string
|
URL string
|
||||||
Interval int
|
Interval int
|
||||||
|
Reject bool // Reject group
|
||||||
|
Direct bool // Direct group
|
||||||
|
Tags []string // Tags for the group
|
||||||
|
Default bool // Default group
|
||||||
}
|
}
|
||||||
|
|
||||||
type GroupType string
|
type GroupType string
|
||||||
|
|||||||
@ -79,7 +79,7 @@ func BuildSingbox(adapter proxy.Adapter, uuid string) ([]byte, error) {
|
|||||||
|
|
||||||
rawConfig["outbounds"] = proxies
|
rawConfig["outbounds"] = proxies
|
||||||
route := RouteOptions{
|
route := RouteOptions{
|
||||||
Final: "手动选择",
|
Final: adapter.Default,
|
||||||
Rules: []Rule{
|
Rules: []Rule{
|
||||||
{
|
{
|
||||||
Inbound: []string{
|
Inbound: []string{
|
||||||
@ -114,7 +114,7 @@ func BuildSingbox(adapter proxy.Adapter, uuid string) ([]byte, error) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
ClashMode: "global",
|
ClashMode: "global",
|
||||||
Outbound: "手动选择",
|
Outbound: adapter.Default,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
IPIsPrivate: true,
|
IPIsPrivate: true,
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"embed"
|
"embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
@ -23,42 +22,22 @@ var shadowsocksSupportMethod = []string{"aes-128-gcm", "aes-192-gcm", "aes-256-g
|
|||||||
func BuildSurfboard(servers proxy.Adapter, siteName string, user UserInfo) []byte {
|
func BuildSurfboard(servers proxy.Adapter, siteName string, user UserInfo) []byte {
|
||||||
var proxies, proxyGroup string
|
var proxies, proxyGroup string
|
||||||
var removed []string
|
var removed []string
|
||||||
for _, node := range servers.Proxies {
|
var ps []string
|
||||||
if uri := buildProxy(node, user.UUID); uri != "" {
|
|
||||||
proxies += uri
|
for _, p := range servers.Proxies {
|
||||||
} else {
|
switch p.Protocol {
|
||||||
removed = append(removed, node.Name)
|
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)
|
||||||
}
|
}
|
||||||
|
ps = append(ps, p.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, group := range servers.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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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")
|
file, err := configFiles.ReadFile("default.tpl")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("read default surfboard config error: %v", err.Error())
|
logger.Errorf("read default surfboard config error: %v", err.Error())
|
||||||
@ -78,42 +57,24 @@ func BuildSurfboard(servers proxy.Adapter, siteName string, user UserInfo) []byt
|
|||||||
} else {
|
} else {
|
||||||
expiredAt = user.ExpiredDate.Format("2006-01-02 15:04:05")
|
expiredAt = user.ExpiredDate.Format("2006-01-02 15:04:05")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ps = tool.RemoveStringElement(ps, removed...)
|
||||||
|
proxyGroup = strings.Join(ps, ",")
|
||||||
|
|
||||||
// convert traffic
|
// convert traffic
|
||||||
upload := traffic.AutoConvert(user.Upload, false)
|
upload := traffic.AutoConvert(user.Upload, false)
|
||||||
download := traffic.AutoConvert(user.Download, false)
|
download := traffic.AutoConvert(user.Download, false)
|
||||||
total := traffic.AutoConvert(user.TotalTraffic, false)
|
total := traffic.AutoConvert(user.TotalTraffic, false)
|
||||||
unusedTraffic := traffic.AutoConvert(user.TotalTraffic-user.Upload-user.Download, false)
|
unusedTraffic := traffic.AutoConvert(user.TotalTraffic-user.Upload-user.Download, false)
|
||||||
// query Host
|
// query Host
|
||||||
urlParse, err := url.Parse(user.SubscribeURL)
|
if err = tpl.Execute(&buf, map[string]interface{}{
|
||||||
if err != nil {
|
"Proxies": proxies,
|
||||||
return nil
|
"ProxyGroup": proxyGroup,
|
||||||
}
|
"SubscribeURL": user.SubscribeURL,
|
||||||
if err := tpl.Execute(&buf, map[string]interface{}{
|
"SubscribeInfo": fmt.Sprintf("title=%s订阅信息, content=上传流量:%s\\n下载流量:%s\\n剩余流量: %s\\n套餐流量:%s\\n到期时间:%s", siteName, upload, download, unusedTraffic, total, expiredAt),
|
||||||
"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 {
|
}); err != nil {
|
||||||
logger.Errorf("build surfboard config error: %v", err.Error())
|
logger.Errorf("build Surge config error: %v", err.Error())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return buf.Bytes()
|
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
|
#!MANAGED-CONFIG {{.SubscribeURL}} interval=43200 strict=true
|
||||||
|
|
||||||
[General]
|
[General]
|
||||||
loglevel = notify
|
dns-server = system, 119.29.29.29, 223.5.5.5
|
||||||
ipv6 = false
|
skip-proxy = 192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12, 127.0.0.0/8, localhost, *.local
|
||||||
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
|
always-real-ip = *.lan, lens.l.google.com, *.srv.nintendo.net, *.stun.playstation.net, *.xboxlive.com, xbox.*.*.microsoft.com, *.msftncsi.com, *.msftconnecttest.com
|
||||||
tls-provider = default
|
proxy-test-url = http://www.gstatic.com/generate_204
|
||||||
show-error-page-for-reject = true
|
internet-test-url = http://connectivitycheck.platform.hicloud.com/generate_204
|
||||||
dns-server = 223.6.6.6, 119.29.29.29, 119.28.28.28
|
|
||||||
test-timeout = 5
|
test-timeout = 5
|
||||||
internet-test-url = http://bing.com
|
http-listen = 0.0.0.0:6088
|
||||||
proxy-test-url = http://bing.com
|
socks5-listen = 0.0.0.0:6089
|
||||||
|
|
||||||
[Panel]
|
[Panel]
|
||||||
SubscribeInfo = {{ .SubscribeInfo }}, style=info
|
SubscribeInfo = {{.SubscribeInfo}}, style=info
|
||||||
|
|
||||||
# Surfboard 配置文档:https://manual.getsurfboard.com/
|
|
||||||
|
|
||||||
[Proxy]
|
[Proxy]
|
||||||
# 代理列表
|
{{.Proxies}}
|
||||||
{{ .Proxies }}
|
|
||||||
|
|
||||||
[Proxy Group]
|
[Proxy Group]
|
||||||
# 代理组列表
|
🚀 Proxy = select, 🌏 Auto, 🎯 Direct, include-other-group=🇺🇳 Nodes
|
||||||
{{ .ProxyGroup }}
|
🍎 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]
|
[Rule]
|
||||||
# 规则列表
|
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Apple/Apple_All.list, 🍎 Apple
|
||||||
{{ .Rules }}
|
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
|
#!MANAGED-CONFIG {{.SubscribeURL}} interval=43200 strict=true
|
||||||
# Surge 的规则配置手册: https://manual.nssurge.com/
|
|
||||||
|
|
||||||
[General]
|
[General]
|
||||||
loglevel = notify
|
loglevel = notify
|
||||||
# 从 Surge iOS 4 / Surge Mac 3.3.0 起,工具开始支持 DoH
|
external-controller-access = purinio@0.0.0.0:6170
|
||||||
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
|
|
||||||
exclude-simple-hostnames = true
|
exclude-simple-hostnames = true
|
||||||
|
show-error-page-for-reject = true
|
||||||
|
udp-priority = true
|
||||||
|
udp-policy-not-supported-behaviour = reject
|
||||||
ipv6 = true
|
ipv6 = true
|
||||||
|
ipv6-vif = auto
|
||||||
test-timeout = 4
|
|
||||||
proxy-test-url = http://www.gstatic.com/generate_204
|
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]
|
# > Surge Mac Parameters
|
||||||
hide-apple-request = true
|
http-listen = 0.0.0.0:6088
|
||||||
hide-crashlytics-request = true
|
socks5-listen = 0.0.0.0:6089
|
||||||
use-keyword-filter = false
|
|
||||||
hide-udp = false
|
# > Surge iOS Parameters
|
||||||
|
allow-wifi-access = true
|
||||||
|
allow-hotspot-access = true
|
||||||
|
wifi-access-http-port = 6088
|
||||||
|
wifi-access-socks5-port = 6089
|
||||||
|
|
||||||
[Panel]
|
[Panel]
|
||||||
SubscribeInfo = {{ .SubscribeInfo }}, style=info
|
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]
|
[Proxy]
|
||||||
{{ .Proxies }}
|
{{.Proxies}}
|
||||||
|
|
||||||
[Proxy Group]
|
[Proxy Group]
|
||||||
# 代理组列表
|
🚀 Proxy = select, 🌏 Auto, 🎯 Direct, include-other-group=🇺🇳 Nodes
|
||||||
{{ .ProxyGroup }}
|
🍎 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]
|
[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]
|
[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"
|
"bytes"
|
||||||
"embed"
|
"embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/perfect-panel/server/pkg/tool"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/perfect-panel/server/pkg/adapter/proxy"
|
"github.com/perfect-panel/server/pkg/adapter/proxy"
|
||||||
"github.com/perfect-panel/server/pkg/logger"
|
"github.com/perfect-panel/server/pkg/logger"
|
||||||
|
"github.com/perfect-panel/server/pkg/tool"
|
||||||
"github.com/perfect-panel/server/pkg/traffic"
|
"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 {
|
func (m *Surge) Build(siteName string, user UserInfo) []byte {
|
||||||
var proxies, proxyGroup, rules string
|
var proxies, proxyGroup string
|
||||||
var removed []string
|
var removed []string
|
||||||
|
var ps []string
|
||||||
|
|
||||||
for _, p := range m.Adapter.Proxies {
|
for _, p := range m.Adapter.Proxies {
|
||||||
switch p.Protocol {
|
switch p.Protocol {
|
||||||
case "shadowsocks":
|
case "shadowsocks":
|
||||||
proxies += buildShadowsocks(p, uuid)
|
proxies += buildShadowsocks(p, user.UUID)
|
||||||
case "trojan":
|
case "trojan":
|
||||||
proxies += buildTrojan(p, uuid)
|
proxies += buildTrojan(p, user.UUID)
|
||||||
case "hysteria2":
|
case "hysteria2":
|
||||||
proxies += buildHysteria2(p, uuid)
|
proxies += buildHysteria2(p, user.UUID)
|
||||||
case "vmess":
|
case "vmess":
|
||||||
proxies += buildVMess(p, uuid)
|
proxies += buildVMess(p, user.UUID)
|
||||||
default:
|
default:
|
||||||
removed = append(removed, p.Name)
|
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")
|
file, err := configFiles.ReadFile("default.tpl")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -99,23 +78,21 @@ func (m *Surge) Build(uuid, siteName string, user UserInfo) []byte {
|
|||||||
} else {
|
} else {
|
||||||
expiredAt = user.ExpiredDate.Format("2006-01-02 15:04:05")
|
expiredAt = user.ExpiredDate.Format("2006-01-02 15:04:05")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ps = tool.RemoveStringElement(ps, removed...)
|
||||||
|
proxyGroup = strings.Join(ps, ",")
|
||||||
|
|
||||||
// convert traffic
|
// convert traffic
|
||||||
upload := traffic.AutoConvert(user.Upload, false)
|
upload := traffic.AutoConvert(user.Upload, false)
|
||||||
download := traffic.AutoConvert(user.Download, false)
|
download := traffic.AutoConvert(user.Download, false)
|
||||||
total := traffic.AutoConvert(user.TotalTraffic, false)
|
total := traffic.AutoConvert(user.TotalTraffic, false)
|
||||||
unusedTraffic := traffic.AutoConvert(user.TotalTraffic-user.Upload-user.Download, false)
|
unusedTraffic := traffic.AutoConvert(user.TotalTraffic-user.Upload-user.Download, false)
|
||||||
// query Host
|
// 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,
|
"Proxies": proxies,
|
||||||
"ProxyGroup": proxyGroup,
|
"ProxyGroup": proxyGroup,
|
||||||
"SubscribeURL": user.SubscribeURL,
|
"SubscribeURL": user.SubscribeURL,
|
||||||
"SubscribeInfo": fmt.Sprintf("title=%s订阅信息, content=上传流量:%s\\n下载流量:%s\\n剩余流量: %s\\n套餐流量:%s\\n到期时间:%s", siteName, upload, download, unusedTraffic, total, expiredAt),
|
"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 {
|
}); err != nil {
|
||||||
logger.Errorf("build Surge config error: %v", err.Error())
|
logger.Errorf("build Surge config error: %v", err.Error())
|
||||||
return nil
|
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 (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/model/server"
|
"github.com/perfect-panel/server/internal/model/server"
|
||||||
@ -11,14 +13,21 @@ import (
|
|||||||
"github.com/perfect-panel/server/pkg/tool"
|
"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 {
|
func addNode(data *server.Server, host string, port int) *proxy.Proxy {
|
||||||
var option any
|
var option any
|
||||||
|
tags := strings.Split(data.Tags, ",")
|
||||||
|
if len(tags) > 0 {
|
||||||
|
tags = tool.RemoveDuplicateElements(tags...)
|
||||||
|
}
|
||||||
|
|
||||||
node := proxy.Proxy{
|
node := proxy.Proxy{
|
||||||
Name: data.Name,
|
Name: data.Name,
|
||||||
Server: host,
|
Server: host,
|
||||||
Port: port,
|
Port: port,
|
||||||
Country: data.Country,
|
Country: data.Country,
|
||||||
Protocol: data.Protocol,
|
Protocol: data.Protocol,
|
||||||
|
Tags: tags,
|
||||||
}
|
}
|
||||||
switch data.Protocol {
|
switch data.Protocol {
|
||||||
case "shadowsocks":
|
case "shadowsocks":
|
||||||
@ -75,85 +84,78 @@ func addNode(data *server.Server, host string, port int) *proxy.Proxy {
|
|||||||
node.Port = tuic.Port
|
node.Port = tuic.Port
|
||||||
}
|
}
|
||||||
option = tuic
|
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:
|
default:
|
||||||
|
fmt.Printf("[Error] 不支持的协议: %s", data.Protocol)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
node.Option = option
|
node.Option = option
|
||||||
return &node
|
return &node
|
||||||
}
|
}
|
||||||
|
|
||||||
func addProxyToGroup(proxyName, groupName string, groups []proxy.Group) []proxy.Group {
|
func adapterRules(groups []*server.RuleGroup) (proxyGroup []proxy.Group, rules []string, defaultGroup string) {
|
||||||
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) {
|
|
||||||
for _, group := range groups {
|
for _, group := range groups {
|
||||||
proxyGroup = append(proxyGroup, proxy.Group{
|
if group.Default {
|
||||||
Name: group.Name,
|
log.Printf("[Debug] 规则组 %s 是默认组", group.Name)
|
||||||
Type: proxy.GroupTypeSelect,
|
defaultGroup = group.Name
|
||||||
Proxies: RemoveEmptyString(strings.Split(group.Tags, ",")),
|
}
|
||||||
})
|
switch group.Type {
|
||||||
|
case server.RuleGroupTypeReject:
|
||||||
|
proxyGroup = append(proxyGroup, proxy.Group{
|
||||||
|
Name: group.Name,
|
||||||
|
Type: proxy.GroupTypeSelect,
|
||||||
|
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")...)
|
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) {
|
// generateDefaultGroup generates a default proxy group with auto-selection and manual selection options.
|
||||||
for tag, servers := range tags {
|
func generateDefaultGroup() (proxyGroup []proxy.Group) {
|
||||||
proxies := adapterProxies(servers)
|
proxyGroup = append(proxyGroup, proxy.Group{
|
||||||
if len(proxies) != 0 {
|
Name: AutoSelect,
|
||||||
for _, p := range proxies {
|
Type: proxy.GroupTypeURLTest,
|
||||||
group = addProxyToGroup(p.Name, tag, group)
|
Proxies: make([]string, 0),
|
||||||
}
|
URL: "https://www.gstatic.com/generate_204",
|
||||||
}
|
Interval: 300,
|
||||||
}
|
})
|
||||||
return group
|
|
||||||
|
return proxyGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateProxyGroup(servers []proxy.Proxy) (proxyGroup []proxy.Group, region []string) {
|
func adapterProxies(servers []*server.Server) ([]proxy.Proxy, []string, map[string][]string) {
|
||||||
// 设置手动选择分组
|
|
||||||
proxyGroup = append(proxyGroup, []proxy.Group{
|
|
||||||
{
|
|
||||||
Name: "智能线路",
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
proxyGroup = addProxyToGroup(node.Name, "手动选择", proxyGroup)
|
|
||||||
}
|
|
||||||
proxyGroup = addProxyToGroup("DIRECT", "手动选择", proxyGroup)
|
|
||||||
return proxyGroup, tool.RemoveDuplicateElements(region...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func adapterProxies(servers []*server.Server) []proxy.Proxy {
|
|
||||||
var proxies []proxy.Proxy
|
var proxies []proxy.Proxy
|
||||||
|
var tags = make(map[string][]string)
|
||||||
for _, node := range servers {
|
for _, node := range servers {
|
||||||
switch node.RelayMode {
|
switch node.RelayMode {
|
||||||
case server.RelayModeAll:
|
case server.RelayModeAll:
|
||||||
@ -168,8 +170,20 @@ func adapterProxies(servers []*server.Server) []proxy.Proxy {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if relay.Prefix != "" {
|
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)
|
proxies = append(proxies, *n)
|
||||||
}
|
}
|
||||||
case server.RelayModeRandom:
|
case server.RelayModeRandom:
|
||||||
@ -185,18 +199,46 @@ func adapterProxies(servers []*server.Server) []proxy.Proxy {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if relay.Prefix != "" {
|
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)
|
proxies = append(proxies, *n)
|
||||||
default:
|
default:
|
||||||
logger.Info("Not Relay Mode", logger.Field("node", node.Name), logger.Field("relayMode", node.RelayMode))
|
logger.Info("Not Relay Mode", logger.Field("node", node.Name), logger.Field("relayMode", node.RelayMode))
|
||||||
n := addNode(node, node.ServerAddr, 0)
|
n := addNode(node, node.ServerAddr, 0)
|
||||||
if n != nil {
|
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)
|
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 切片去除空值
|
// RemoveEmptyString 切片去除空值
|
||||||
@ -210,18 +252,61 @@ func RemoveEmptyString(arr []string) []string {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func RemoveEmptyGroup(arr []proxy.Group) []proxy.Group {
|
// SortGroups sorts the provided slice of proxy groups by their names.
|
||||||
var result []proxy.Group
|
func SortGroups(groups []proxy.Group, nodes []string, tags map[string][]string, defaultName string) []proxy.Group {
|
||||||
var removeNames []string
|
var sortedGroups []proxy.Group
|
||||||
for _, group := range arr {
|
var defaultGroup, autoSelectGroup proxy.Group
|
||||||
if group.Name == "手动选择" {
|
// 在所有分组找到默认分组并将他放到第一个
|
||||||
group.Proxies = tool.RemoveStringElement(group.Proxies, removeNames...)
|
for _, group := range groups {
|
||||||
|
if group.Name == "" || group.Name == "DIRECT" || group.Name == "REJECT" {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
if len(group.Proxies) > 0 {
|
// 如果是默认分组
|
||||||
result = append(result, group)
|
if group.Default {
|
||||||
} else {
|
group.Proxies = append([]string{AutoSelect}, nodes...)
|
||||||
removeNames = append(removeNames, group.Name)
|
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...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
return result
|
|
||||||
|
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
|
// Gracefully shut down all WebSocket connections
|
||||||
func (dm *DeviceManager) Shutdown(ctx context.Context) {
|
func (dm *DeviceManager) Shutdown(ctx context.Context) {
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
|
|||||||
@ -282,7 +282,6 @@ const (
|
|||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`
|
`
|
||||||
|
|
||||||
DefaultTrafficExceedEmailTemplate = `<!doctype html>
|
DefaultTrafficExceedEmailTemplate = `<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
|||||||
@ -33,4 +33,7 @@ func RegisterHandlers(mux *asynq.ServeMux, serverCtx *svc.ServiceContext) {
|
|||||||
|
|
||||||
// Schedule total server data
|
// Schedule total server data
|
||||||
mux.Handle(types.SchedulerTotalServerData, traffic.NewServerDataLogic(serverCtx))
|
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 (
|
const (
|
||||||
SchedulerCheckSubscription = "scheduler:check:subscription"
|
SchedulerCheckSubscription = "scheduler:check:subscription"
|
||||||
SchedulerTotalServerData = "scheduler:total:server"
|
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 {
|
if _, err := m.server.Register("@every 180s", totalServerDataTask); err != nil {
|
||||||
logger.Errorf("register total server data task failed: %s", err.Error())
|
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 {
|
if err := m.server.Run(); err != nil {
|
||||||
logger.Errorf("run scheduler failed: %s", err.Error())
|
logger.Errorf("run scheduler failed: %s", err.Error())
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user