diff --git a/.github/workflows/develop.yaml b/.github/workflows/develop.yaml index e06d843..7a7b958 100644 --- a/.github/workflows/develop.yaml +++ b/.github/workflows/develop.yaml @@ -26,25 +26,26 @@ jobs: - name: Build Docker image - run: docker build -t ${{ secrets.DOCKER_USERNAME }}/ppanel-server-dev:${{ env.COMMIT_ID }} . + run: docker build --build-arg VERSION=${{ env.COMMIT_ID }} -t ${{ secrets.DOCKER_USERNAME }}/ppanel-server-dev:${{ env.COMMIT_ID }} . - name: Push Docker image run: docker push ${{ secrets.DOCKER_USERNAME }}/ppanel-server-dev:${{ env.COMMIT_ID }} - - name: Deploy to server - uses: appleboy/ssh-action@v0.1.6 - with: - host: ${{ secrets.SSH_HOST }} - username: ${{ secrets.SSH_USER }} - key: ${{ secrets.SSH_PRIVATE_KEY }} - script: | - if [ $(docker ps -a -q -f name=ppanel-server-dev) ]; then - echo "Stopping and removing existing ppanel-server container..." - docker stop ppanel-server-dev - docker rm ppanel-server-dev - else - echo "No existing ppanel-server-dev container running." - fi - - docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} - docker run -d --restart=always --log-driver=journald --name ppanel-server-dev -p 8080:8080 -v /www/wwwroot/api/etc:/app/etc --restart=always -d ${{ secrets.DOCKER_USERNAME }}/ppanel-server-dev:${{ env.COMMIT_ID }} \ No newline at end of file +# - name: Deploy to server +# uses: appleboy/ssh-action@v0.1.6 +# with: +# host: ${{ secrets.SSH_HOST }} +# username: ${{ secrets.SSH_USER }} +# key: ${{ secrets.SSH_PRIVATE_KEY }} +# script: | +# if [ $(docker ps -a -q -f name=ppanel-server-dev) ]; then +# echo "Stopping and removing existing ppanel-server container..." +# docker stop ppanel-server-dev +# docker rm ppanel-server-dev +# else +# echo "No existing ppanel-server-dev container running." +# fi +# +# docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} +# docker run -d --restart=always --log-driver=journald --name ppanel-server-dev -p 8080:8080 -v /www/wwwroot/api/etc:/app/etc -v /www/wwwroot/api/logs:/app/logs --restart=always -d ${{ secrets.DOCKER_USERNAME }}/ppanel-server-dev:${{ env.COMMIT_ID }} +# \ No newline at end of file diff --git a/.github/workflows/swagger.yaml b/.github/workflows/swagger.yaml index 9ddeff2..177c2b0 100644 --- a/.github/workflows/swagger.yaml +++ b/.github/workflows/swagger.yaml @@ -35,7 +35,6 @@ jobs: mkdir -p swagger goctl api plugin -plugin goctl-swagger='swagger -filename common.json -pack Response -response "[{\"name\":\"code\",\"type\":\"integer\",\"description\":\"状态码\"},{\"name\":\"msg\",\"type\":\"string\",\"description\":\"消息\"},{\"name\":\"data\",\"type\":\"object\",\"description\":\"数据\",\"is_data\":true}]";' -api ./apis/swagger_common.api -dir ./swagger goctl api plugin -plugin goctl-swagger='swagger -filename user.json -pack Response -response "[{\"name\":\"code\",\"type\":\"integer\",\"description\":\"状态码\"},{\"name\":\"msg\",\"type\":\"string\",\"description\":\"消息\"},{\"name\":\"data\",\"type\":\"object\",\"description\":\"数据\",\"is_data\":true}]";' -api ./apis/swagger_user.api -dir ./swagger - goctl api plugin -plugin goctl-swagger='swagger -filename app.json -pack Response -response "[{\"name\":\"code\",\"type\":\"integer\",\"description\":\"状态码\"},{\"name\":\"msg\",\"type\":\"string\",\"description\":\"消息\"},{\"name\":\"data\",\"type\":\"object\",\"description\":\"数据\",\"is_data\":true}]";' -api ./apis/swagger_app.api -dir ./swagger goctl api plugin -plugin goctl-swagger='swagger -filename admin.json -pack Response -response "[{\"name\":\"code\",\"type\":\"integer\",\"description\":\"状态码\"},{\"name\":\"msg\",\"type\":\"string\",\"description\":\"消息\"},{\"name\":\"data\",\"type\":\"object\",\"description\":\"数据\",\"is_data\":true}]";' -api ./apis/swagger_admin.api -dir ./swagger goctl api plugin -plugin goctl-swagger='swagger -filename ppanel.json -pack Response -response "[{\"name\":\"code\",\"type\":\"integer\",\"description\":\"状态码\"},{\"name\":\"msg\",\"type\":\"string\",\"description\":\"消息\"},{\"name\":\"data\",\"type\":\"object\",\"description\":\"数据\",\"is_data\":true}]";' -api ppanel.api -dir ./swagger goctl api plugin -plugin goctl-swagger='swagger -filename node.json -pack Response -response "[{\"name\":\"code\",\"type\":\"integer\",\"description\":\"状态码\"},{\"name\":\"msg\",\"type\":\"string\",\"description\":\"消息\"},{\"name\":\"data\",\"type\":\"object\",\"description\":\"数据\",\"is_data\":true}]";' -api ./apis/swagger_node.api -dir ./swagger @@ -45,7 +44,6 @@ jobs: run: | test -f ./swagger/common.json test -f ./swagger/user.json - test -f ./swagger/app.json test -f ./swagger/admin.json - name: Checkout target repository diff --git a/.run/go build github.com_perfect-panel_server.run.xml b/.run/go build github.com_perfect-panel_server.run.xml deleted file mode 100644 index 454ba95..0000000 --- a/.run/go build github.com_perfect-panel_server.run.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 118076e..74d41df 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,7 +20,7 @@ RUN go mod download COPY . . # Build the binary with version and build time -RUN BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") && \ +RUN BUILD_TIME=$(date -u +"%Y-%m-%d %H:%M:%S") && \ go build -ldflags="-s -w -X 'github.com/perfect-panel/server/pkg/constant.Version=${VERSION}' -X 'github.com/perfect-panel/server/pkg/constant.BuildTime=${BUILD_TIME}'" -o /app/ppanel ppanel.go # Final minimal image diff --git a/README.md b/README.md index 8c8b806..975f5a7 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,19 @@ +> **Article 1.** +> All human beings are born free and equal in dignity and rights. +> They are endowed with reason and conscience and should act towards one another in a spirit of brotherhood. +> +> **Article 12.** +> No one shall be subjected to arbitrary interference with his privacy, family, home or correspondence, nor to attacks upon his honour and reputation. +> Everyone has the right to the protection of the law against such interference or attacks. +> +> **Article 19.** +> Everyone has the right to freedom of opinion and expression; this right includes freedom to hold opinions without interference and to seek, receive and impart information and ideas through any media and regardless of frontiers. +> +> *Source: [United Nations – Universal Declaration of Human Rights (UN.org)](https://www.un.org/sites/un2.un.org/files/2021/03/udhr.pdf)* + ## 📋 Overview PPanel Server is the backend component of the PPanel project, providing robust APIs and core functionality for managing @@ -101,8 +114,8 @@ proxy services. Built with Go, it emphasizes performance, security, and scalabil 4. **Pull from Docker Hub** (after CI/CD publishes): ```bash - docker pull yourusername/ppanel-server:latest - docker run --rm -p 8080:8080 yourusername/ppanel-server:latest + docker pull ppanel/ppanel-server:latest + docker run --rm -p 8080:8080 ppanel/ppanel-server:latest ``` ## 📖 API Documentation diff --git a/adapter/adapter.go b/adapter/adapter.go new file mode 100644 index 0000000..da5d049 --- /dev/null +++ b/adapter/adapter.go @@ -0,0 +1,158 @@ +package adapter + +import ( + "strings" + + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/pkg/logger" +) + +type Adapter struct { + SiteName string // 站点名称 + Servers []*node.Node // 服务器列表 + UserInfo User // 用户信息 + ClientTemplate string // 客户端配置模板 + OutputFormat string // 输出格式,默认是 base64 + SubscribeName string // 订阅名称 +} + +type Option func(*Adapter) + +// WithServers 设置服务器列表 +func WithServers(servers []*node.Node) Option { + return func(opts *Adapter) { + opts.Servers = servers + } +} + +// WithUserInfo 设置用户信息 +func WithUserInfo(user User) Option { + return func(opts *Adapter) { + opts.UserInfo = user + } +} + +// WithOutputFormat 设置输出格式 +func WithOutputFormat(format string) Option { + return func(opts *Adapter) { + opts.OutputFormat = format + } +} + +// WithSiteName 设置站点名称 +func WithSiteName(name string) Option { + return func(opts *Adapter) { + opts.SiteName = name + } +} + +// WithSubscribeName 设置订阅名称 +func WithSubscribeName(name string) Option { + return func(opts *Adapter) { + opts.SubscribeName = name + } +} + +func NewAdapter(tpl string, opts ...Option) *Adapter { + adapter := &Adapter{ + Servers: []*node.Node{}, + UserInfo: User{}, + ClientTemplate: tpl, + OutputFormat: "base64", // 默认输出格式 + } + + for _, opt := range opts { + opt(adapter) + } + + return adapter +} + +func (adapter *Adapter) Client() (*Client, error) { + client := &Client{ + SiteName: adapter.SiteName, + SubscribeName: adapter.SubscribeName, + ClientTemplate: adapter.ClientTemplate, + OutputFormat: adapter.OutputFormat, + Proxies: []Proxy{}, + UserInfo: adapter.UserInfo, + } + + proxies, err := adapter.Proxies(adapter.Servers) + if err != nil { + return nil, err + } + client.Proxies = proxies + return client, nil +} + +func (adapter *Adapter) Proxies(servers []*node.Node) ([]Proxy, error) { + var proxies []Proxy + + for _, item := range servers { + if item.Server == nil { + logger.Errorf("[Adapter] Server is nil for node ID: %d", item.Id) + continue + } + protocols, err := item.Server.UnmarshalProtocols() + if err != nil { + logger.Errorf("[Adapter] Unmarshal Protocols error: %s; server id : %d", err.Error(), item.ServerId) + continue + } + for _, protocol := range protocols { + if protocol.Type == item.Protocol { + proxies = append(proxies, Proxy{ + Sort: item.Sort, + Name: item.Name, + Server: item.Address, + Port: item.Port, + Type: item.Protocol, + Tags: strings.Split(item.Tags, ","), + Security: protocol.Security, + SNI: protocol.SNI, + AllowInsecure: protocol.AllowInsecure, + Fingerprint: protocol.Fingerprint, + RealityServerAddr: protocol.RealityServerAddr, + RealityServerPort: protocol.RealityServerPort, + RealityPrivateKey: protocol.RealityPrivateKey, + RealityPublicKey: protocol.RealityPublicKey, + RealityShortId: protocol.RealityShortId, + Transport: protocol.Transport, + Host: protocol.Host, + Path: protocol.Path, + ServiceName: protocol.ServiceName, + Method: protocol.Cipher, + ServerKey: protocol.ServerKey, + Flow: protocol.Flow, + HopPorts: protocol.HopPorts, + HopInterval: protocol.HopInterval, + ObfsPassword: protocol.ObfsPassword, + UpMbps: protocol.UpMbps, + DownMbps: protocol.DownMbps, + DisableSNI: protocol.DisableSNI, + ReduceRtt: protocol.ReduceRtt, + UDPRelayMode: protocol.UDPRelayMode, + CongestionController: protocol.CongestionController, + PaddingScheme: protocol.PaddingScheme, + Multiplex: protocol.Multiplex, + XhttpMode: protocol.XhttpMode, + XhttpExtra: protocol.XhttpExtra, + Encryption: protocol.Encryption, + EncryptionMode: protocol.EncryptionMode, + EncryptionRtt: protocol.EncryptionRtt, + EncryptionTicket: protocol.EncryptionTicket, + EncryptionServerPadding: protocol.EncryptionServerPadding, + EncryptionPrivateKey: protocol.EncryptionPrivateKey, + EncryptionClientPadding: protocol.EncryptionClientPadding, + EncryptionPassword: protocol.EncryptionPassword, + Ratio: protocol.Ratio, + CertMode: protocol.CertMode, + CertDNSProvider: protocol.CertDNSProvider, + CertDNSEnv: protocol.CertDNSEnv, + }) + } + } + } + + return proxies, nil +} diff --git a/adapter/adapter_test.go b/adapter/adapter_test.go new file mode 100644 index 0000000..d45649b --- /dev/null +++ b/adapter/adapter_test.go @@ -0,0 +1,34 @@ +package adapter + +import ( + "testing" + "time" +) + +func TestAdapter_Client(t *testing.T) { + servers := getServers() + if len(servers) == 0 { + t.Errorf("[Test] No servers found") + return + } + a := NewAdapter(tpl, WithServers(servers), WithUserInfo(User{ + Password: "test-password", + ExpiredAt: time.Now().AddDate(1, 0, 0), + Download: 0, + Upload: 0, + Traffic: 1000, + SubscribeURL: "https://example.com/subscribe", + })) + client, err := a.Client() + if err != nil { + t.Errorf("[Test] Failed to get client: %v", err.Error()) + return + } + bytes, err := client.Build() + if err != nil { + t.Errorf("[Test] Failed to build client config: %v", err.Error()) + return + } + t.Logf("[Test] Client config built successfully: %s", string(bytes)) + +} diff --git a/adapter/client.go b/adapter/client.go new file mode 100644 index 0000000..e456898 --- /dev/null +++ b/adapter/client.go @@ -0,0 +1,146 @@ +package adapter + +import ( + "bytes" + "encoding/base64" + "reflect" + "text/template" + "time" + + "github.com/Masterminds/sprig/v3" +) + +type Proxy struct { + Sort int + Name string + Server string + Port uint16 + Type string + Tags []string + + // Security Options + Security string + SNI string // Server Name Indication for TLS + AllowInsecure bool // Allow insecure connections (skip certificate verification) + Fingerprint string // Client fingerprint for TLS connections + RealityServerAddr string // Reality server address + RealityServerPort int // Reality server port + RealityPrivateKey string // Reality private key for authentication + RealityPublicKey string // Reality public key for authentication + RealityShortId string // Reality short ID for authentication + // Transport Options + Transport string // Transport protocol (e.g., ws, http, grpc) + Host string // For WebSocket/HTTP/HTTPS + Path string // For HTTP/HTTPS + ServiceName string // For gRPC + // Shadowsocks Options + Method string + ServerKey string // For Shadowsocks 2022 + + // Vmess/Vless/Trojan Options + Flow string // Flow for Vmess/Vless/Trojan + // Hysteria2 Options + HopPorts string // Comma-separated list of hop ports + HopInterval int // Interval for hop ports in seconds + ObfsPassword string // Obfuscation password for Hysteria2 + UpMbps int // Upload speed in Mbps + DownMbps int // Download speed in Mbps + + // Tuic Options + DisableSNI bool // Disable SNI + ReduceRtt bool // Reduce RTT + UDPRelayMode string // UDP relay mode (e.g., "full", "partial") + CongestionController string // Congestion controller (e.g., "cubic", "bbr") + + // AnyTLS + PaddingScheme string + + // Mieru + Multiplex string + + // Obfs + //Obfs string // obfs, 'none', 'http', 'tls' + //ObfsHost string // obfs host + //ObfsPath string // obfs path + + // Vless + XhttpMode string // xhttp mode + XhttpExtra string // xhttp path + + // encryption + Encryption string // encryption,'none', 'mlkem768x25519plus' + EncryptionMode string // encryption mode,'native', 'xorpub', 'random' + EncryptionRtt string // encryption rtt,'0rtt', '1rtt' + EncryptionTicket string // encryption ticket + EncryptionServerPadding string // encryption server padding + EncryptionPrivateKey string // encryption private key + EncryptionClientPadding string // encryption client padding + EncryptionPassword string // encryption password + + Ratio float64 // Traffic ratio, default is 1 + CertMode string // Certificate mode, `none`|`http`|`dns`|`self` + CertDNSProvider string // DNS provider for certificate + CertDNSEnv string // Environment for DNS provider +} + +type User struct { + Password string + ExpiredAt time.Time + Download int64 + Upload int64 + Traffic int64 + SubscribeURL string +} + +type Client struct { + SiteName string // Name of the site + SubscribeName string // Name of the subscription + ClientTemplate string // Template for the entire client configuration + OutputFormat string // json, yaml, etc. + Proxies []Proxy // List of proxy configurations + UserInfo User // User information +} + +func (c *Client) Build() ([]byte, error) { + var buf bytes.Buffer + tmpl, err := template.New("client").Funcs(sprig.TxtFuncMap()).Parse(c.ClientTemplate) + if err != nil { + return nil, err + } + + proxies := make([]map[string]interface{}, len(c.Proxies)) + for i, p := range c.Proxies { + proxies[i] = StructToMap(p) + } + + err = tmpl.Execute(&buf, map[string]interface{}{ + "SiteName": c.SiteName, + "SubscribeName": c.SubscribeName, + "OutputFormat": c.OutputFormat, + "Proxies": proxies, + "UserInfo": c.UserInfo, + }) + if err != nil { + return nil, err + } + + result := buf.String() + if c.OutputFormat == "base64" { + encoded := base64.StdEncoding.EncodeToString([]byte(result)) + return []byte(encoded), nil + } + + return buf.Bytes(), nil +} + +func StructToMap(obj interface{}) map[string]interface{} { + m := make(map[string]interface{}) + v := reflect.ValueOf(obj) + t := reflect.TypeOf(obj) + + for i := 0; i < v.NumField(); i++ { + field := t.Field(i) + m[field.Name] = v.Field(i).Interface() + } + return m +} diff --git a/adapter/client_test.go b/adapter/client_test.go new file mode 100644 index 0000000..beb9145 --- /dev/null +++ b/adapter/client_test.go @@ -0,0 +1,153 @@ +package adapter + +import ( + "testing" + "time" +) + +var tpl = ` +{{- range $n := .Proxies }} + {{- $dn := urlquery (default "node" $n.Name) -}} + {{- $sni := default $n.Host $n.SNI -}} + + {{- if eq $n.Type "shadowsocks" -}} + {{- $userinfo := b64enc (print $n.Method ":" $.UserInfo.Password) -}} + {{- printf "ss://%s@%s:%v#%s" $userinfo $n.Host $n.Port $dn -}} + {{- "\n" -}} + {{- end -}} + + {{- if eq $n.Type "trojan" -}} + {{- $qs := "security=tls" -}} + {{- if $sni }}{{ $qs = printf "%s&sni=%s" $qs (urlquery $sni) }}{{ end -}} + {{- if $n.AllowInsecure }}{{ $qs = printf "%s&allowInsecure=%v" $qs $n.AllowInsecure }}{{ end -}} + {{- if $n.Fingerprint }}{{ $qs = printf "%s&fp=%s" $qs (urlquery $n.Fingerprint) }}{{ end -}} + {{- printf "trojan://%s@%s:%v?%s#%s" $.UserInfo.Password $n.Host $n.Port $qs $dn -}} + {{- "\n" -}} + {{- end -}} + + {{- if eq $n.Type "vless" -}} + {{- $qs := "encryption=none" -}} + {{- if $n.RealityPublicKey -}} + {{- $qs = printf "%s&security=reality" $qs -}} + {{- $qs = printf "%s&pbk=%s" $qs (urlquery $n.RealityPublicKey) -}} + {{- if $n.RealityShortId }}{{ $qs = printf "%s&sid=%s" $qs (urlquery $n.RealityShortId) }}{{ end -}} + {{- else -}} + {{- if or $n.SNI $n.Fingerprint $n.AllowInsecure }} + {{- $qs = printf "%s&security=tls" $qs -}} + {{- end -}} + {{- end -}} + {{- if $n.SNI }}{{ $qs = printf "%s&sni=%s" $qs (urlquery $n.SNI) }}{{ end -}} + {{- if $n.AllowInsecure }}{{ $qs = printf "%s&allowInsecure=%v" $qs $n.AllowInsecure }}{{ end -}} + {{- if $n.Fingerprint }}{{ $qs = printf "%s&fp=%s" $qs (urlquery $n.Fingerprint) }}{{ end -}} + {{- if $n.Network }}{{ $qs = printf "%s&type=%s" $qs $n.Network }}{{ end -}} + {{- if $n.Path }}{{ $qs = printf "%s&path=%s" $qs (urlquery $n.Path) }}{{ end -}} + {{- if $n.ServiceName }}{{ $qs = printf "%s&serviceName=%s" $qs (urlquery $n.ServiceName) }}{{ end -}} + {{- if $n.Flow }}{{ $qs = printf "%s&flow=%s" $qs (urlquery $n.Flow) }}{{ end -}} + {{- printf "vless://%s@%s:%v?%s#%s" $n.ServerKey $n.Host $n.Port $qs $dn -}} + {{- "\n" -}} + {{- end -}} + + {{- if eq $n.Type "vmess" -}} + {{- $obj := dict + "v" "2" + "ps" $n.Name + "add" $n.Host + "port" $n.Port + "id" $n.ServerKey + "aid" 0 + "net" (or $n.Network "tcp") + "type" "none" + "path" (or $n.Path "") + "host" $n.Host + -}} + {{- if or $n.SNI $n.Fingerprint $n.AllowInsecure }}{{ set $obj "tls" "tls" }}{{ end -}} + {{- if $n.SNI }}{{ set $obj "sni" $n.SNI }}{{ end -}} + {{- if $n.Fingerprint }}{{ set $obj "fp" $n.Fingerprint }}{{ end -}} + {{- printf "vmess://%s" (b64enc (toJson $obj)) -}} + {{- "\n" -}} + {{- end -}} + + {{- if or (eq $n.Type "hysteria2") (eq $n.Type "hy2") -}} + {{- $qs := "" -}} + {{- if $n.SNI }}{{ $qs = printf "sni=%s" (urlquery $n.SNI) }}{{ end -}} + {{- if $n.AllowInsecure }}{{ $qs = printf "%s&insecure=%v" $qs $n.AllowInsecure }}{{ end -}} + {{- if $n.ObfsPassword }}{{ $qs = printf "%s&obfs-password=%s" $qs (urlquery $n.ObfsPassword) }}{{ end -}} + {{- printf "hy2://%s@%s:%v%s#%s" + $.UserInfo.Password + $n.Host + $n.Port + (ternary (gt (len $qs) 0) (print "?" $qs) "") + $dn -}} + {{- "\n" -}} + {{- end -}} + + {{- if eq $n.Type "tuic" -}} + {{- $qs := "" -}} + {{- if $n.SNI }}{{ $qs = printf "sni=%s" (urlquery $n.SNI) }}{{ end -}} + {{- if $n.AllowInsecure }}{{ $qs = printf "%s&insecure=%v" $qs $n.AllowInsecure }}{{ end -}} + {{- printf "tuic://%s:%s@%s:%v%s#%s" + $n.ServerKey + $.UserInfo.Password + $n.Host + $n.Port + (ternary (gt (len $qs) 0) (print "?" $qs) "") + $dn -}} + {{- "\n" -}} + {{- end -}} + + {{- if eq $n.Type "anytls" -}} + {{- $qs := "" -}} + {{- if $n.SNI }}{{ $qs = printf "sni=%s" (urlquery $n.SNI) }}{{ end -}} + {{- printf "anytls://%s@%s:%v%s#%s" + $.UserInfo.Password + $n.Host + $n.Port + (ternary (gt (len $qs) 0) (print "?" $qs) "") + $dn -}} + {{- "\n" -}} + {{- end -}} + +{{- end }} +` + +func TestClient_Build(t *testing.T) { + client := &Client{ + SiteName: "TestSite", + SubscribeName: "TestSubscribe", + ClientTemplate: tpl, + Proxies: []Proxy{ + { + Name: "TestShadowSocks", + Type: "shadowsocks", + Host: "127.0.0.1", + Port: 1234, + Method: "aes-256-gcm", + }, + { + Name: "TestTrojan", + Type: "trojan", + Host: "example.com", + Port: 443, + AllowInsecure: true, + Security: "tls", + Transport: "tcp", + SNI: "v1-dy.ixigua.com", + }, + }, + UserInfo: User{ + Password: "testpassword", + ExpiredAt: time.Now().Add(24 * time.Hour), + Download: 1000000, + Upload: 500000, + Traffic: 1500000, + SubscribeURL: "https://example.com/subscribe", + }, + } + buf, err := client.Build() + if err != nil { + t.Fatalf("Failed to build client: %v", err) + } + + t.Logf("[测试] 输出: %s", buf) + +} diff --git a/adapter/utils.go b/adapter/utils.go new file mode 100644 index 0000000..b8e8da3 --- /dev/null +++ b/adapter/utils.go @@ -0,0 +1 @@ +package adapter diff --git a/adapter/utils_test.go b/adapter/utils_test.go new file mode 100644 index 0000000..7a4e32c --- /dev/null +++ b/adapter/utils_test.go @@ -0,0 +1,46 @@ +package adapter + +import ( + "testing" + + "github.com/perfect-panel/server/internal/model/server" + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +func TestAdapterProxy(t *testing.T) { + + servers := getServers() + if len(servers) == 0 { + t.Fatal("no servers found") + } + for _, srv := range servers { + proxy, err := adapterProxy(*srv, "example.com", 0) + if err != nil { + t.Errorf("failed to adapt server %s: %v", srv.Name, err) + } + t.Logf("[测试] 适配服务器 %s 成功: %+v", srv.Name, proxy) + } + +} + +func getServers() []*server.Server { + db, err := connectMySQL("root:mylove520@tcp(localhost:3306)/perfectlink?charset=utf8mb4&parseTime=True&loc=Local") + if err != nil { + return nil + } + var servers []*server.Server + if err = db.Model(&server.Server{}).Find(&servers).Error; err != nil { + return nil + } + return servers +} +func connectMySQL(dsn string) (*gorm.DB, error) { + db, err := gorm.Open(mysql.New(mysql.Config{ + DSN: dsn, + }), &gorm.Config{}) + if err != nil { + return nil, err + } + return db, nil +} diff --git a/apis/admin/application.api b/apis/admin/application.api new file mode 100644 index 0000000..8ac5355 --- /dev/null +++ b/apis/admin/application.api @@ -0,0 +1,96 @@ +syntax = "v1" + +info ( + title: "Application API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +import "../types.api" + +type ( + SubscribeApplication { + Id int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Icon string `json:"icon,omitempty"` + Scheme string `json:"scheme,omitempty"` + UserAgent string `json:"user_agent"` + IsDefault bool `json:"is_default"` + SubscribeTemplate string `json:"template"` + OutputFormat string `json:"output_format"` + DownloadLink DownloadLink `json:"download_link,omitempty"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + } + GetSubscribeApplicationListResponse { + Total int64 `json:"total"` + List []SubscribeApplication `json:"list"` + } + CreateSubscribeApplicationRequest { + Name string `json:"name"` + Description string `json:"description,omitempty"` + Icon string `json:"icon,omitempty"` + Scheme string `json:"scheme,omitempty"` + UserAgent string `json:"user_agent"` + IsDefault bool `json:"is_default"` + SubscribeTemplate string `json:"template"` + OutputFormat string `json:"output_format"` + DownloadLink DownloadLink `json:"download_link"` + } + UpdateSubscribeApplicationRequest { + Id int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Icon string `json:"icon,omitempty"` + Scheme string `json:"scheme,omitempty"` + UserAgent string `json:"user_agent"` + IsDefault bool `json:"is_default"` + SubscribeTemplate string `json:"template"` + OutputFormat string `json:"output_format"` + DownloadLink DownloadLink `json:"download_link,omitempty"` + } + DeleteSubscribeApplicationRequest { + Id int64 `json:"id"` + } + GetSubscribeApplicationListRequest { + Page int `form:"page"` + Size int `form:"size"` + } + PreviewSubscribeTemplateRequest { + Id int64 `form:"id"` + } + PreviewSubscribeTemplateResponse { + Template string `json:"template"` // 预览的模板内容 + } +) + +@server ( + prefix: v1/admin/application + group: admin/application + middleware: AuthMiddleware +) +service ppanel { + @doc "Create subscribe application" + @handler CreateSubscribeApplication + post / (CreateSubscribeApplicationRequest) returns (SubscribeApplication) + + @doc "Update subscribe application" + @handler UpdateSubscribeApplication + put /subscribe_application (UpdateSubscribeApplicationRequest) returns (SubscribeApplication) + + @doc "Get subscribe application list" + @handler GetSubscribeApplicationList + get /subscribe_application_list (GetSubscribeApplicationListRequest) returns (GetSubscribeApplicationListResponse) + + @doc "Delete subscribe application" + @handler DeleteSubscribeApplication + delete /subscribe_application (DeleteSubscribeApplicationRequest) + + @doc "Preview Template" + @handler PreviewSubscribeTemplate + get /preview (PreviewSubscribeTemplateRequest) returns (PreviewSubscribeTemplateResponse) +} + diff --git a/apis/admin/console.api b/apis/admin/console.api index c20bb14..1a8ded7 100644 --- a/apis/admin/console.api +++ b/apis/admin/console.api @@ -21,7 +21,7 @@ type ( Download int64 `json:"download"` } ServerTotalDataResponse { - OnlineUserIPs int64 `json:"online_user_ips"` + OnlineUsers int64 `json:"online_users"` OnlineServers int64 `json:"online_servers"` OfflineServers int64 `json:"offline_servers"` TodayUpload int64 `json:"today_upload"` diff --git a/apis/admin/log.api b/apis/admin/log.api index 12fc312..3b117c8 100644 --- a/apis/admin/log.api +++ b/apis/admin/log.api @@ -12,19 +12,184 @@ import "../types.api" type ( GetMessageLogListRequest { - Page int `form:"page"` - Size int `form:"size"` - Type string `form:"type"` - Platform string `form:"platform,omitempty"` - To string `form:"to,omitempty"` - Subject string `form:"subject,omitempty"` - Content string `form:"content,omitempty"` - Status int `form:"status,omitempty"` + Page int `form:"page"` + Size int `form:"size"` + Type uint8 `form:"type"` + Search string `form:"search,optional"` } GetMessageLogListResponse { Total int64 `json:"total"` List []MessageLog `json:"list"` } + FilterLogParams { + Page int `form:"page"` + Size int `form:"size"` + Date string `form:"date,optional"` + Search string `form:"search,optional"` + } + FilterEmailLogResponse { + Total int64 `json:"total"` + List []MessageLog `json:"list"` + } + FilterMobileLogResponse { + Total int64 `json:"total"` + List []MessageLog `json:"list"` + } + SubscribeLog { + UserId int64 `json:"user_id"` + Token string `json:"token"` + UserAgent string `json:"user_agent"` + ClientIP string `json:"client_ip"` + UserSubscribeId int64 `json:"user_subscribe_id"` + Timestamp int64 `json:"timestamp"` + } + FilterSubscribeLogRequest { + FilterLogParams + UserId int64 `form:"user_id,optional"` + UserSubscribeId int64 `form:"user_subscribe_id,optional"` + } + FilterSubscribeLogResponse { + Total int64 `json:"total"` + List []SubscribeLog `json:"list"` + } + LoginLog { + UserId int64 `json:"user_id"` + Method string `json:"method"` + LoginIP string `json:"login_ip"` + UserAgent string `json:"user_agent"` + Success bool `json:"success"` + Timestamp int64 `json:"timestamp"` + } + FilterLoginLogRequest { + FilterLogParams + UserId int64 `form:"user_id,optional"` + } + FilterLoginLogResponse { + Total int64 `json:"total"` + List []LoginLog `json:"list"` + } + RegisterLog { + UserId int64 `json:"user_id"` + AuthMethod string `json:"auth_method"` + Identifier string `json:"identifier"` + RegisterIP string `json:"register_ip"` + UserAgent string `json:"user_agent"` + Timestamp int64 `json:"timestamp"` + } + FilterRegisterLogRequest { + FilterLogParams + UserId int64 `form:"user_id,optional"` + } + FilterRegisterLogResponse { + Total int64 `json:"total"` + List []RegisterLog `json:"list"` + } + ResetSubscribeLog { + Type uint16 `json:"type"` + UserId int64 `json:"user_id"` + UserSubscribeId int64 `json:"user_subscribe_id"` + OrderNo string `json:"order_no,omitempty"` + Timestamp int64 `json:"timestamp"` + } + FilterResetSubscribeLogRequest { + FilterLogParams + UserSubscribeId int64 `form:"user_subscribe_id,optional"` + } + FilterResetSubscribeLogResponse { + Total int64 `json:"total"` + List []ResetSubscribeLog `json:"list"` + } + UserSubscribeTrafficLog { + SubscribeId int64 `json:"subscribe_id"` // Subscribe ID + UserId int64 `json:"user_id"` // User ID + Upload int64 `json:"upload"` // Upload traffic in bytes + Download int64 `json:"download"` // Download traffic in bytes + Total int64 `json:"total"` // Total traffic in bytes (Upload + Download) + Date string `json:"date"` // Date in YYYY-MM-DD format + Details bool `json:"details"` // Whether to show detailed traffic + } + FilterSubscribeTrafficRequest { + FilterLogParams + UserId int64 `form:"user_id,optional"` + UserSubscribeId int64 `form:"user_subscribe_id,optional"` + } + FilterSubscribeTrafficResponse { + Total int64 `json:"total"` + List []UserSubscribeTrafficLog `json:"list"` + } + ServerTrafficLog { + ServerId int64 `json:"server_id"` // Server ID + Upload int64 `json:"upload"` // Upload traffic in bytes + Download int64 `json:"download"` // Download traffic in bytes + Total int64 `json:"total"` // Total traffic in bytes (Upload + Download) + Date string `json:"date"` // Date in YYYY-MM-DD format + Details bool `json:"details"` // Whether to show detailed traffic + } + FilterServerTrafficLogRequest { + FilterLogParams + ServerId int64 `form:"server_id,optional"` + } + FilterServerTrafficLogResponse { + Total int64 `json:"total"` + List []ServerTrafficLog `json:"list"` + } + FilterBalanceLogRequest { + FilterLogParams + UserId int64 `form:"user_id,optional"` + } + FilterBalanceLogResponse { + Total int64 `json:"total"` + List []BalanceLog `json:"list"` + } + FilterCommissionLogRequest { + FilterLogParams + UserId int64 `form:"user_id,optional"` + } + FilterCommissionLogResponse { + Total int64 `json:"total"` + List []CommissionLog `json:"list"` + } + GiftLog { + Type uint16 `json:"type"` + userId int64 `json:"user_id"` + OrderNo string `json:"order_no"` + SubscribeId int64 `json:"subscribe_id"` + Amount int64 `json:"amount"` + Balance int64 `json:"balance"` + Remark string `json:"remark,omitempty"` + Timestamp int64 `json:"timestamp"` + } + FilterGiftLogRequest { + FilterLogParams + UserId int64 `form:"user_id,optional"` + } + FilterGiftLogResponse { + Total int64 `json:"total"` + List []GiftLog `json:"list"` + } + TrafficLogDetails { + Id int64 `json:"id"` + ServerId int64 `json:"server_id"` + UserId int64 `json:"user_id"` + SubscribeId int64 `json:"subscribe_id"` + Download int64 `json:"download"` + Upload int64 `json:"upload"` + Timestamp int64 `json:"timestamp"` + } + FilterTrafficLogDetailsRequest { + FilterLogParams + ServerId int64 `form:"server_id,optional"` + SubscribeId int64 `form:"subscribe_id,optional"` + UserId int64 `form:"user_id,optional"` + } + FilterTrafficLogDetailsResponse { + Total int64 `json:"total"` + List []TrafficLogDetails `json:"list"` + } + LogSetting { + AutoClear *bool `json:"auto_clear"` + ClearDays int64 `json:"clear_days"` + } ) @server ( @@ -36,5 +201,61 @@ service ppanel { @doc "Get message log list" @handler GetMessageLogList get /message/list (GetMessageLogListRequest) returns (GetMessageLogListResponse) + + @doc "Filter email log" + @handler FilterEmailLog + get /email/list (FilterLogParams) returns (FilterEmailLogResponse) + + @doc "Filter mobile log" + @handler FilterMobileLog + get /mobile/list (FilterLogParams) returns (FilterMobileLogResponse) + + @doc "Filter subscribe log" + @handler FilterSubscribeLog + get /subscribe/list (FilterSubscribeLogRequest) returns (FilterSubscribeLogResponse) + + @doc "Filter login log" + @handler FilterLoginLog + get /login/list (FilterLoginLogRequest) returns (FilterLoginLogResponse) + + @doc "Filter register log" + @handler FilterRegisterLog + get /register/list (FilterRegisterLogRequest) returns (FilterRegisterLogResponse) + + @doc "Filter reset subscribe log" + @handler FilterResetSubscribeLog + get /subscribe/reset/list (FilterResetSubscribeLogRequest) returns (FilterResetSubscribeLogResponse) + + @doc "Filter user subscribe traffic log" + @handler FilterUserSubscribeTrafficLog + get /subscribe/traffic/list (FilterSubscribeTrafficRequest) returns (FilterSubscribeTrafficResponse) + + @doc "Filter server traffic log" + @handler FilterServerTrafficLog + get /server/traffic/list (FilterServerTrafficLogRequest) returns (FilterServerTrafficLogResponse) + + @doc "Filter balance log" + @handler FilterBalanceLog + get /balance/list (FilterBalanceLogRequest) returns (FilterBalanceLogResponse) + + @doc "Filter commission log" + @handler FilterCommissionLog + get /commission/list (FilterCommissionLogRequest) returns (FilterCommissionLogResponse) + + @doc "Filter gift log" + @handler FilterGiftLog + get /gift/list (FilterGiftLogRequest) returns (FilterGiftLogResponse) + + @doc "Filter traffic log details" + @handler FilterTrafficLogDetails + get /traffic/details (FilterTrafficLogDetailsRequest) returns (FilterTrafficLogDetailsResponse) + + @doc "Get log setting" + @handler GetLogSetting + get /setting returns (LogSetting) + + @doc "Update log setting" + @handler UpdateLogSetting + post /setting (LogSetting) } diff --git a/apis/admin/marketing.api b/apis/admin/marketing.api new file mode 100644 index 0000000..8014c0d --- /dev/null +++ b/apis/admin/marketing.api @@ -0,0 +1,167 @@ +syntax = "v1" + +info ( + title: "Marketing API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +type ( + CreateBatchSendEmailTaskRequest { + Subject string `json:"subject"` + Content string `json:"content"` + Scope int8 `json:"scope"` + RegisterStartTime int64 `json:"register_start_time,omitempty"` + RegisterEndTime int64 `json:"register_end_time,omitempty"` + Additional string `json:"additional,omitempty"` + Scheduled int64 `json:"scheduled,omitempty"` + Interval uint8 `json:"interval,omitempty"` + Limit uint64 `json:"limit,omitempty"` + } + BatchSendEmailTask { + Id int64 `json:"id"` + Subject string `json:"subject"` + Content string `json:"content"` + Recipients string `json:"recipients"` + Scope int8 `json:"scope"` + RegisterStartTime int64 `json:"register_start_time"` + RegisterEndTime int64 `json:"register_end_time"` + Additional string `json:"additional"` + Scheduled int64 `json:"scheduled"` + Interval uint8 `json:"interval"` + Limit uint64 `json:"limit"` + Status uint8 `json:"status"` + Errors string `json:"errors"` + Total uint64 `json:"total"` + Current uint64 `json:"current"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + } + GetBatchSendEmailTaskListRequest { + Page int `form:"page"` + Size int `form:"size"` + Scope *int8 `form:"scope,omitempty"` + Status *uint8 `form:"status,omitempty"` + } + GetBatchSendEmailTaskListResponse { + Total int64 `json:"total"` + List []BatchSendEmailTask `json:"list"` + } + StopBatchSendEmailTaskRequest { + Id int64 `json:"id"` + } + GetPreSendEmailCountRequest { + Scope int8 `json:"scope"` + RegisterStartTime int64 `json:"register_start_time,omitempty"` + RegisterEndTime int64 `json:"register_end_time,omitempty"` + } + GetPreSendEmailCountResponse { + Count int64 `json:"count"` + } + GetBatchSendEmailTaskStatusRequest { + Id int64 `json:"id"` + } + GetBatchSendEmailTaskStatusResponse { + Status uint8 `json:"status"` + Current int64 `json:"current"` + Total int64 `json:"total"` + Errors string `json:"errors"` + } + CreateQuotaTaskRequest { + Subscribers []int64 `json:"subscribers"` + IsActive *bool `json:"is_active"` + StartTime int64 `json:"start_time"` + EndTime int64 `json:"end_time"` + ResetTraffic bool `json:"reset_traffic"` + Days uint64 `json:"days"` + GiftType uint8 `json:"gift_type"` + GiftValue uint64 `json:"gift_value"` + } + QuotaTask { + Id int64 `json:"id"` + Subscribers []int64 `json:"subscribers"` + IsActive *bool `json:"is_active"` + StartTime int64 `json:"start_time"` + EndTime int64 `json:"end_time"` + ResetTraffic bool `json:"reset_traffic"` + Days uint64 `json:"days"` + GiftType uint8 `json:"gift_type"` + GiftValue uint64 `json:"gift_value"` + Objects []int64 `json:"objects"` // UserSubscribe IDs + Status uint8 `json:"status"` + Total int64 `json:"total"` + Current int64 `json:"current"` + Errors string `json:"errors"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + } + QueryQuotaTaskPreCountRequest { + Subscribers []int64 `json:"subscribers"` + IsActive *bool `json:"is_active"` + StartTime int64 `json:"start_time"` + EndTime int64 `json:"end_time"` + } + QueryQuotaTaskPreCountResponse { + Count int64 `json:"count"` + } + QueryQuotaTaskListRequest { + Page int `form:"page"` + Size int `form:"size"` + Status *uint8 `form:"status,omitempty"` + } + QueryQuotaTaskListResponse { + Total int64 `json:"total"` + List []QuotaTask `json:"list"` + } + QueryQuotaTaskStatusRequest { + Id int64 `json:"id"` + } + QueryQuotaTaskStatusResponse { + Status uint8 `json:"status"` + Current int64 `json:"current"` + Total int64 `json:"total"` + Errors string `json:"errors"` + } +) + +@server ( + prefix: v1/admin/marketing + group: admin/marketing + middleware: AuthMiddleware +) +service ppanel { + @doc "Create a batch send email task" + @handler CreateBatchSendEmailTask + post /email/batch/send (CreateBatchSendEmailTaskRequest) + + @doc "Get batch send email task list" + @handler GetBatchSendEmailTaskList + get /email/batch/list (GetBatchSendEmailTaskListRequest) returns (GetBatchSendEmailTaskListResponse) + + @doc "Stop a batch send email task" + @handler StopBatchSendEmailTask + post /email/batch/stop (StopBatchSendEmailTaskRequest) + + @doc "Get pre-send email count" + @handler GetPreSendEmailCount + post /email/batch/pre-send-count (GetPreSendEmailCountRequest) returns (GetPreSendEmailCountResponse) + + @doc "Get batch send email task status" + @handler GetBatchSendEmailTaskStatus + post /email/batch/status (GetBatchSendEmailTaskStatusRequest) returns (GetBatchSendEmailTaskStatusResponse) + + @doc "Create a quota task" + @handler CreateQuotaTask + post /quota/create (CreateQuotaTaskRequest) + + @doc "Query quota task pre-count" + @handler QueryQuotaTaskPreCount + post /quota/pre-count (QueryQuotaTaskPreCountRequest) returns (QueryQuotaTaskPreCountResponse) + + @doc "Query quota task list" + @handler QueryQuotaTaskList + get /quota/list (QueryQuotaTaskListRequest) returns (QueryQuotaTaskListResponse) +} + diff --git a/apis/admin/server.api b/apis/admin/server.api index 52b7715..2877427 100644 --- a/apis/admin/server.api +++ b/apis/admin/server.api @@ -11,108 +11,134 @@ info ( import "../types.api" type ( - GetNodeServerListRequest { - Page int `form:"page" validate:"required"` - Size int `form:"size" validate:"required"` - Tags string `form:"tags,omitempty"` - GroupId int64 `form:"group_id,omitempty"` - Search string `form:"search,omitempty"` + ServerOnlineIP { + IP string `json:"ip"` + Protocol string `json:"protocol"` } - GetNodeServerListResponse { + ServerOnlineUser { + IP []ServerOnlineIP `json:"ip"` + UserId int64 `json:"user_id"` + Subscribe string `json:"subscribe"` + SubscribeId int64 `json:"subscribe_id"` + Traffic int64 `json:"traffic"` + ExpiredAt int64 `json:"expired_at"` + } + ServerStatus { + Cpu float64 `json:"cpu"` + Mem float64 `json:"mem"` + Disk float64 `json:"disk"` + Protocol string `json:"protocol"` + Online []ServerOnlineUser `json:"online"` + Status string `json:"status"` + } + Server { + Id int64 `json:"id"` + Name string `json:"name"` + Country string `json:"country"` + City string `json:"city"` + Address string `json:"address"` + Sort int `json:"sort"` + Protocols []Protocol `json:"protocols"` + LastReportedAt int64 `json:"last_reported_at"` + Status ServerStatus `json:"status"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + } + CreateServerRequest { + Name string `json:"name"` + Country string `json:"country,omitempty"` + City string `json:"city,omitempty"` + Address string `json:"address"` + Sort int `json:"sort,omitempty"` + Protocols []Protocol `json:"protocols"` + } + UpdateServerRequest { + Id int64 `json:"id"` + Name string `json:"name"` + Country string `json:"country,omitempty"` + City string `json:"city,omitempty"` + Address string `json:"address"` + Sort int `json:"sort,omitempty"` + Protocols []Protocol `json:"protocols"` + } + DeleteServerRequest { + Id int64 `json:"id"` + } + FilterServerListRequest { + Page int `form:"page"` + Size int `form:"size"` + Search string `form:"search,omitempty"` + } + FilterServerListResponse { Total int64 `json:"total"` List []Server `json:"list"` } - UpdateNodeRequest { - Id int64 `json:"id" validate:"required"` - Tags []string `json:"tags"` - Country string `json:"country"` - City string `json:"city"` - Name string `json:"name" validate:"required"` - ServerAddr string `json:"server_addr" validate:"required"` - RelayMode string `json:"relay_mode"` - RelayNode []NodeRelay `json:"relay_node"` - SpeedLimit int `json:"speed_limit"` - TrafficRatio float32 `json:"traffic_ratio"` - GroupId int64 `json:"group_id"` - Protocol string `json:"protocol" validate:"required"` - Config interface{} `json:"config" validate:"required"` - Enable *bool `json:"enable"` - Sort int64 `json:"sort"` + GetServerProtocolsRequest { + Id int64 `form:"id"` + } + GetServerProtocolsResponse { + Protocols []Protocol `json:"protocols"` + } + Node { + Id int64 `json:"id"` + Name string `json:"name"` + Tags []string `json:"tags"` + Port uint16 `json:"port"` + Address string `json:"address"` + ServerId int64 `json:"server_id"` + Protocol string `json:"protocol"` + Enabled *bool `json:"enabled"` + Sort int `json:"sort,omitempty"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` } CreateNodeRequest { - Name string `json:"name" validate:"required"` - Tags []string `json:"tags"` - Country string `json:"country"` - City string `json:"city"` - ServerAddr string `json:"server_addr" validate:"required"` - RelayMode string `json:"relay_mode"` - RelayNode []NodeRelay `json:"relay_node"` - SpeedLimit int `json:"speed_limit"` - TrafficRatio float32 `json:"traffic_ratio"` - GroupId int64 `json:"group_id"` - Protocol string `json:"protocol" validate:"required"` - Config interface{} `json:"config" validate:"required"` - Enable *bool `json:"enable"` - Sort int64 `json:"sort"` + Name string `json:"name"` + Tags []string `json:"tags,omitempty"` + Port uint16 `json:"port"` + Address string `json:"address"` + ServerId int64 `json:"server_id"` + Protocol string `json:"protocol"` + Enabled *bool `json:"enabled"` + } + UpdateNodeRequest { + Id int64 `json:"id"` + Name string `json:"name"` + Tags []string `json:"tags,omitempty"` + Port uint16 `json:"port"` + Address string `json:"address"` + ServerId int64 `json:"server_id"` + Protocol string `json:"protocol"` + Enabled *bool `json:"enabled"` + } + ToggleNodeStatusRequest { + Id int64 `json:"id"` + Enable *bool `json:"enable"` } DeleteNodeRequest { - Id int64 `json:"id" validate:"required"` + Id int64 `json:"id"` } - GetNodeGroupListResponse { - Total int64 `json:"total"` - List []ServerGroup `json:"list"` + FilterNodeListRequest { + Page int `form:"page"` + Size int `form:"size"` + Search string `form:"search,omitempty"` } - CreateNodeGroupRequest { - Name string `json:"name" validate:"required"` - Description string `json:"description"` + FilterNodeListResponse { + Total int64 `json:"total"` + List []Node `json:"list"` } - UpdateNodeGroupRequest { - Id int64 `json:"id" validate:"required"` - Name string `json:"name" validate:"required"` - Description string `json:"description"` + HasMigrateSeverNodeResponse { + HasMigrate bool `json:"has_migrate"` } - DeleteNodeGroupRequest { - Id int64 `json:"id" validate:"required"` + MigrateServerNodeResponse { + Succee uint64 `json:"succee"` + Fail uint64 `json:"fail"` + Message string `json:"message,omitempty"` } - BatchDeleteNodeRequest { - Ids []int64 `json:"ids" validate:"required"` - } - BatchDeleteNodeGroupRequest { - Ids []int64 `json:"ids" validate:"required"` - } - GetNodeDetailRequest { - Id int64 `form:"id" validate:"required"` - } - NodeSortRequest { + ResetSortRequest { Sort []SortItem `json:"sort"` } - CreateRuleGroupRequest { - Name string `json:"name" validate:"required"` - Icon string `json:"icon"` - Type string `json:"type"` - Tags []string `json:"tags"` - Rules string `json:"rules"` - Default bool `json:"default"` - Enable bool `json:"enable"` - } - UpdateRuleGroupRequest { - Id int64 `json:"id" validate:"required"` - Icon string `json:"icon"` - Type string `json:"type"` - Name string `json:"name" validate:"required"` - Tags []string `json:"tags"` - Rules string `json:"rules"` - Default bool `json:"default"` - Enable bool `json:"enable"` - } - DeleteRuleGroupRequest { - Id int64 `json:"id" validate:"required"` - } - GetRuleGroupResponse { - Total int64 `json:"total"` - List []ServerRuleGroup `json:"list"` - } - GetNodeTagListResponse { + QueryNodeTagResponse { Tags []string `json:"tags"` } ) @@ -123,72 +149,64 @@ type ( middleware: AuthMiddleware ) service ppanel { - @doc "Get node tag list" - @handler GetNodeTagList - get /tag/list returns (GetNodeTagListResponse) + @doc "Create Server" + @handler CreateServer + post /create (CreateServerRequest) - @doc "Get node list" - @handler GetNodeList - get /list (GetNodeServerListRequest) returns (GetNodeServerListResponse) + @doc "Update Server" + @handler UpdateServer + post /update (UpdateServerRequest) - @doc "Get node detail" - @handler GetNodeDetail - get /detail (GetNodeDetailRequest) returns (Server) + @doc "Delete Server" + @handler DeleteServer + post /delete (DeleteServerRequest) - @doc "Update node" - @handler UpdateNode - put / (UpdateNodeRequest) + @doc "Filter Server List" + @handler FilterServerList + get /list (FilterServerListRequest) returns (FilterServerListResponse) - @doc "Create node" + @doc "Get Server Protocols" + @handler GetServerProtocols + get /protocols (GetServerProtocolsRequest) returns (GetServerProtocolsResponse) + + @doc "Create Node" @handler CreateNode - post / (CreateNodeRequest) + post /node/create (CreateNodeRequest) - @doc "Delete node" + @doc "Update Node" + @handler UpdateNode + post /node/update (UpdateNodeRequest) + + @doc "Delete Node" @handler DeleteNode - delete / (DeleteNodeRequest) + post /node/delete (DeleteNodeRequest) - @doc "Batch delete node" - @handler BatchDeleteNode - delete /batch (BatchDeleteNodeRequest) + @doc "Filter Node List" + @handler FilterNodeList + get /node/list (FilterNodeListRequest) returns (FilterNodeListResponse) - @doc "Get node group list" - @handler GetNodeGroupList - get /group/list returns (GetNodeGroupListResponse) + @doc "Toggle Node Status" + @handler ToggleNodeStatus + post /node/status/toggle (ToggleNodeStatusRequest) - @doc "Create node group" - @handler CreateNodeGroup - post /group (CreateNodeGroupRequest) + @doc "Check if there is any server or node to migrate" + @handler HasMigrateSeverNode + get /migrate/has returns (HasMigrateSeverNodeResponse) - @doc "Update node group" - @handler UpdateNodeGroup - put /group (UpdateNodeGroupRequest) + @doc "Migrate server and node data to new database" + @handler MigrateServerNode + post /migrate/run returns (MigrateServerNodeResponse) - @doc "Delete node group" - @handler DeleteNodeGroup - delete /group (DeleteNodeGroupRequest) + @doc "Reset server sort" + @handler ResetSortWithServer + post /server/sort (ResetSortRequest) - @doc "Batch delete node group" - @handler BatchDeleteNodeGroup - delete /group/batch (BatchDeleteNodeGroupRequest) + @doc "Reset node sort" + @handler ResetSortWithNode + post /node/sort (ResetSortRequest) - @doc "Node sort " - @handler NodeSort - post /sort (NodeSortRequest) - - @doc "Create rule group" - @handler CreateRuleGroup - post /rule_group (CreateRuleGroupRequest) - - @doc "Update rule group" - @handler UpdateRuleGroup - put /rule_group (UpdateRuleGroupRequest) - - @doc "Delete rule group" - @handler DeleteRuleGroup - delete /rule_group (DeleteRuleGroupRequest) - - @doc "Get rule group list" - @handler GetRuleGroupList - get /rule_group_list returns (GetRuleGroupResponse) + @doc "Query all node tags" + @handler QueryNodeTag + get /node/tags returns (QueryNodeTagResponse) } diff --git a/apis/admin/subscribe.api b/apis/admin/subscribe.api index 4e79c03..bea205d 100644 --- a/apis/admin/subscribe.api +++ b/apis/admin/subscribe.api @@ -35,6 +35,7 @@ type ( } CreateSubscribeRequest { Name string `json:"name" validate:"required"` + Language string `json:"language"` Description string `json:"description"` UnitPrice int64 `json:"unit_price"` UnitTime string `json:"unit_time"` @@ -45,9 +46,8 @@ type ( SpeedLimit int64 `json:"speed_limit"` DeviceLimit int64 `json:"device_limit"` Quota int64 `json:"quota"` - GroupId int64 `json:"group_id"` - ServerGroup []int64 `json:"server_group"` - Server []int64 `json:"server"` + Nodes []int64 `json:"nodes"` + NodeTags []string `json:"node_tags"` Show *bool `json:"show"` Sell *bool `json:"sell"` DeductionRatio int64 `json:"deduction_ratio"` @@ -58,6 +58,7 @@ type ( UpdateSubscribeRequest { Id int64 `json:"id" validate:"required"` Name string `json:"name" validate:"required"` + Language string `json:"language"` Description string `json:"description"` UnitPrice int64 `json:"unit_price"` UnitTime string `json:"unit_time"` @@ -68,9 +69,8 @@ type ( SpeedLimit int64 `json:"speed_limit"` DeviceLimit int64 `json:"device_limit"` Quota int64 `json:"quota"` - GroupId int64 `json:"group_id"` - ServerGroup []int64 `json:"server_group"` - Server []int64 `json:"server"` + Nodes []int64 `json:"nodes"` + NodeTags []string `json:"node_tags"` Show *bool `json:"show"` Sell *bool `json:"sell"` Sort int64 `json:"sort"` @@ -83,10 +83,10 @@ type ( Sort []SortItem `json:"sort"` } GetSubscribeListRequest { - Page int64 `form:"page" validate:"required"` - Size int64 `form:"size" validate:"required"` - GroupId int64 `form:"group_id,omitempty"` - Search string `form:"search,omitempty"` + Page int64 `form:"page" validate:"required"` + Size int64 `form:"size" validate:"required"` + Language string `form:"language,omitempty"` + Search string `form:"search,omitempty"` } SubscribeItem { Subscribe @@ -102,6 +102,9 @@ type ( BatchDeleteSubscribeRequest { Ids []int64 `json:"ids" validate:"required"` } + ResetAllSubscribeTokenResponse { + Success bool `json:"success"` + } ) @server ( @@ -157,5 +160,9 @@ service ppanel { @doc "Subscribe sort" @handler SubscribeSort post /sort (SubscribeSortRequest) + + @doc "Reset all subscribe tokens" + @handler ResetAllSubscribeToken + post /reset_all_token returns (ResetAllSubscribeTokenResponse) } diff --git a/apis/admin/system.api b/apis/admin/system.api index 09323c3..d82d3bd 100644 --- a/apis/admin/system.api +++ b/apis/admin/system.api @@ -11,50 +11,6 @@ info ( import "../types.api" type ( - // Update application request - UpdateApplicationRequest { - Id int64 `json:"id" validate:"required"` - Icon string `json:"icon"` - Name string `json:"name"` - Description string `json:"description"` - SubscribeType string `json:"subscribe_type"` - Platform ApplicationPlatform `json:"platform"` - } - // Create application request - CreateApplicationRequest { - Icon string `json:"icon"` - Name string `json:"name"` - Description string `json:"description"` - SubscribeType string `json:"subscribe_type"` - Platform ApplicationPlatform `json:"platform"` - } - // Update application request - UpdateApplicationVersionRequest { - Id int64 `json:"id" validate:"required"` - Url string `json:"url"` - Version string `json:"version" validate:"required"` - Description string `json:"description"` - Platform string `json:"platform" validate:"required,oneof=windows mac linux android ios harmony"` - IsDefault bool `json:"is_default"` - ApplicationId int64 `json:"application_id" validate:"required"` - } - // Create application request - CreateApplicationVersionRequest { - Url string `json:"url"` - Version string `json:"version" validate:"required"` - Description string `json:"description"` - Platform string `json:"platform" validate:"required,oneof=windows mac linux android ios harmony"` - IsDefault bool `json:"is_default"` - ApplicationId int64 `json:"application_id" validate:"required"` - } - // Delete application request - DeleteApplicationRequest { - Id int64 `json:"id" validate:"required"` - } - // Delete application request - DeleteApplicationVersionRequest { - Id int64 `json:"id" validate:"required"` - } GetNodeMultiplierResponse { Periods []TimePeriod `json:"periods"` } @@ -62,6 +18,15 @@ type ( SetNodeMultiplierRequest { Periods []TimePeriod `json:"periods"` } + PreViewNodeMultiplierResponse { + CurrentTime string `json:"current_time"` + Ratio float32 `json:"ratio"` + } + ModuleConfig { + Secret string `json:"secret"` // 通讯密钥 + ServiceName string `json:"service_name"` // 服务名称 + ServiceVersion string `json:"service_version"` // 服务版本 + } ) @server ( @@ -86,46 +51,6 @@ service ppanel { @handler UpdateSubscribeConfig put /subscribe_config (SubscribeConfig) - @doc "Get subscribe type" - @handler GetSubscribeType - get /subscribe_type returns (SubscribeType) - - @doc "update application config" - @handler UpdateApplicationConfig - put /application_config (ApplicationConfig) - - @doc "get application config" - @handler GetApplicationConfig - get /application_config returns (ApplicationConfig) - - @doc "Get application" - @handler GetApplication - get /application returns (ApplicationResponse) - - @doc "Update application" - @handler UpdateApplication - put /application (UpdateApplicationRequest) - - @doc "Create application" - @handler CreateApplication - post /application (CreateApplicationRequest) - - @doc "Delete application" - @handler DeleteApplication - delete /application (DeleteApplicationRequest) - - @doc "Update application version" - @handler UpdateApplicationVersion - put /application_version (UpdateApplicationVersionRequest) - - @doc "Create application version" - @handler CreateApplicationVersion - post /application_version (CreateApplicationVersionRequest) - - @doc "Delete application" - @handler DeleteApplicationVersion - delete /application_version (DeleteApplicationVersionRequest) - @doc "Get register config" @handler GetRegisterConfig get /register_config returns (RegisterConfig) @@ -201,5 +126,13 @@ service ppanel { @doc "Update Verify Code Config" @handler UpdateVerifyCodeConfig put /verify_code_config (VerifyCodeConfig) + + @doc "PreView Node Multiplier" + @handler PreViewNodeMultiplier + get /node_multiplier/preview returns (PreViewNodeMultiplierResponse) + + @doc "Get Module Config" + @handler GetModuleConfig + get /module returns (ModuleConfig) } diff --git a/apis/admin/tool.api b/apis/admin/tool.api index 1514b76..4d1f17b 100644 --- a/apis/admin/tool.api +++ b/apis/admin/tool.api @@ -14,6 +14,17 @@ type ( LogResponse { List interface{} `json:"list"` } + VersionResponse { + Version string `json:"version"` + } + QueryIPLocationRequest { + IP string `form:"ip" validate:"required"` + } + QueryIPLocationResponse { + Country string `json:"country"` + Region string `json:"region,omitempty"` + City string `json:"city"` + } ) @server ( @@ -29,5 +40,13 @@ service ppanel { @doc "Restart System" @handler RestartSystem get /restart + + @doc "Get Version" + @handler GetVersion + get /version returns (VersionResponse) + + @doc "Query IP Location" + @handler QueryIPLocation + get /ip/location (QueryIPLocationRequest) returns (QueryIPLocationResponse) } diff --git a/apis/admin/user.api b/apis/admin/user.api index 92149fd..dc0c5e8 100644 --- a/apis/admin/user.api +++ b/apis/admin/user.api @@ -32,17 +32,19 @@ type ( Id int64 `form:"id" validate:"required"` } UpdateUserBasiceInfoRequest { - UserId int64 `json:"user_id" validate:"required"` - Password string `json:"password"` - Avatar string `json:"avatar"` - Balance int64 `json:"balance"` - Commission int64 `json:"commission"` - GiftAmount int64 `json:"gift_amount"` - Telegram int64 `json:"telegram"` - ReferCode string `json:"refer_code"` - RefererId int64 `json:"referer_id"` - Enable bool `json:"enable"` - IsAdmin bool `json:"is_admin"` + UserId int64 `json:"user_id" validate:"required"` + Password string `json:"password"` + Avatar string `json:"avatar"` + Balance int64 `json:"balance"` + Commission int64 `json:"commission"` + ReferralPercentage uint8 `json:"referral_percentage"` + OnlyFirstPurchase bool `json:"only_first_purchase"` + GiftAmount int64 `json:"gift_amount"` + Telegram int64 `json:"telegram"` + ReferCode string `json:"refer_code"` + RefererId int64 `json:"referer_id"` + Enable bool `json:"enable"` + IsAdmin bool `json:"is_admin"` } UpdateUserNotifySettingRequest { UserId int64 `json:"user_id" validate:"required"` @@ -52,18 +54,20 @@ type ( EnableTradeNotify bool `json:"enable_trade_notify"` } CreateUserRequest { - Email string `json:"email"` - Telephone string `json:"telephone"` - TelephoneAreaCode string `json:"telephone_area_code"` - Password string `json:"password"` - ProductId int64 `json:"product_id"` - Duration int64 `json:"duration"` - RefererUser string `json:"referer_user"` - ReferCode string `json:"refer_code"` - Balance int64 `json:"balance"` - Commission int64 `json:"commission"` - GiftAmount int64 `json:"gift_amount"` - IsAdmin bool `json:"is_admin"` + Email string `json:"email"` + Telephone string `json:"telephone"` + TelephoneAreaCode string `json:"telephone_area_code"` + Password string `json:"password"` + ProductId int64 `json:"product_id"` + Duration int64 `json:"duration"` + ReferralPercentage uint8 `json:"referral_percentage"` + OnlyFirstPurchase bool `json:"only_first_purchase"` + RefererUser string `json:"referer_user"` + ReferCode string `json:"refer_code"` + Balance int64 `json:"balance"` + Commission int64 `json:"commission"` + GiftAmount int64 `json:"gift_amount"` + IsAdmin bool `json:"is_admin"` } UserSubscribeDetail { Id int64 `json:"id"` @@ -164,6 +168,15 @@ type ( List []UserLoginLog `json:"list"` Total int64 `json:"total"` } + GetUserSubscribeResetTrafficLogsRequest { + Page int `form:"page"` + Size int `form:"size"` + UserSubscribeId int64 `form:"user_subscribe_id"` + } + GetUserSubscribeResetTrafficLogsResponse { + List []ResetSubscribeTrafficLog `json:"list"` + Total int64 `json:"total"` + } DeleteUserSubscribeRequest { UserSubscribeId int64 `json:"user_subscribe_id"` } @@ -251,6 +264,10 @@ service ppanel { @handler GetUserSubscribeLogs get /subscribe/logs (GetUserSubscribeLogsRequest) returns (GetUserSubscribeLogsResponse) + @doc "Get user subcribe reset traffic logs" + @handler GetUserSubscribeResetTrafficLogs + get /subscribe/reset/logs (GetUserSubscribeResetTrafficLogsRequest) returns (GetUserSubscribeResetTrafficLogsResponse) + @doc "Get user subcribe traffic logs" @handler GetUserSubscribeTrafficLogs get /subscribe/traffic_logs (GetUserSubscribeTrafficLogsRequest) returns (GetUserSubscribeTrafficLogsResponse) diff --git a/apis/app/announcement.api b/apis/app/announcement.api deleted file mode 100644 index 52209ed..0000000 --- a/apis/app/announcement.api +++ /dev/null @@ -1,23 +0,0 @@ -syntax = "v1" - -info ( - title: "Announcement API" - desc: "API for ppanel" - author: "Tension" - email: "tension@ppanel.com" - version: "0.0.1" -) - -import "../types.api" - -@server ( - prefix: v1/app/announcement - group: app/announcement - middleware: AppMiddleware,AuthMiddleware -) -service ppanel { - @doc "Query announcement" - @handler QueryAnnouncement - get /list (QueryAnnouncementRequest) returns (QueryAnnouncementResponse) -} - diff --git a/apis/app/auth.api b/apis/app/auth.api deleted file mode 100644 index 779e09e..0000000 --- a/apis/app/auth.api +++ /dev/null @@ -1,104 +0,0 @@ -syntax = "v1" - -info ( - title: "App Auth Api" - desc: "API for ppanel" - author: "Tension" - email: "tension@ppanel.com" - version: "0.0.1" -) - -import ( - "../types.api" -) - -type ( - AppAuthCheckRequest { - Method string `json:"method" validate:"required" validate:"required,oneof=device email mobile"` - Account string `json:"account"` - Identifier string `json:"identifier" validate:"required"` - UserAgent string `json:"user_agent" validate:"required,oneof=windows mac linux android ios harmony"` - AreaCode string `json:"area_code"` - } - AppAuthCheckResponse { - Status bool - } - AppAuthRequest { - Method string `json:"method" validate:"required" validate:"required,oneof=device email mobile"` - Account string `json:"account"` - Password string `json:"password"` - Identifier string `json:"identifier" validate:"required"` - UserAgent string `json:"user_agent" validate:"required,oneof=windows mac linux android ios harmony"` - Code string `json:"code"` - Invite string `json:"invite"` - AreaCode string `json:"area_code"` - CfToken string `json:"cf_token,optional"` - } - AppAuthRespone { - Token string `json:"token"` - } - AppSendCodeRequest { - Method string `json:"method" validate:"required" validate:"required,oneof=email mobile"` - Account string `json:"account"` - AreaCode string `json:"area_code"` - CfToken string `json:"cf_token,optional"` - } - AppSendCodeRespone { - Status bool `json:"status"` - Code string `json:"code,omitempty"` - } - AppConfigRequest { - UserAgent string `json:"user_agent" validate:"required,oneof=windows mac linux android ios harmony"` - } - AppConfigResponse { - EncryptionKey string `json:"encryption_key"` - EncryptionMethod string `json:"encryption_method"` - Domains []string `json:"domains"` - StartupPicture string `json:"startup_picture"` - StartupPictureSkipTime int64 `json:"startup_picture_skip_time"` - Application AppInfo `json:"applications"` - OfficialEmail string `json:"official_email"` - OfficialWebsite string `json:"official_website"` - OfficialTelegram string `json:"official_telegram"` - OfficialTelephone string `json:"official_telephone"` - InvitationLink string `json:"invitation_link"` - KrWebsiteId string `json:"kr_website_id"` - } - AppInfo { - Id int64 `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - Url string `json:"url"` - Version string `json:"version"` - VersionDescription string `json:"version_description"` - IsDefault bool `json:"is_default"` - } -) - -@server ( - prefix: v1/app/auth - group: app/auth - middleware: AppMiddleware -) -service ppanel { - @doc "Check Account" - @handler Check - post /check (AppAuthCheckRequest) returns (AppAuthCheckResponse) - - @doc "Login" - @handler Login - post /login (AppAuthRequest) returns (AppAuthRespone) - - @doc "Register" - @handler Register - post /register (AppAuthRequest) returns (AppAuthRespone) - - @doc "Reset Password" - @handler ResetPassword - post /reset_password (AppAuthRequest) returns (AppAuthRespone) - - @doc "GetAppConfig" - @handler GetAppConfig - post /config (AppConfigRequest) returns (AppConfigResponse) -} - diff --git a/apis/app/document.api b/apis/app/document.api deleted file mode 100644 index 5dfaec0..0000000 --- a/apis/app/document.api +++ /dev/null @@ -1,27 +0,0 @@ -syntax = "v1" - -info ( - title: "Document API" - desc: "API for ppanel" - author: "Tension" - email: "tension@ppanel.com" - version: "0.0.1" -) - -import "../types.api" - -@server ( - prefix: v1/app/document - group: app/document - middleware: AppMiddleware,AuthMiddleware -) -service ppanel { - @doc "Get document list" - @handler QueryDocumentList - get /list returns (QueryDocumentListResponse) - - @doc "Get document detail" - @handler QueryDocumentDetail - get /detail (QueryDocumentDetailRequest) returns (Document) -} - diff --git a/apis/app/node.api b/apis/app/node.api deleted file mode 100644 index df3232b..0000000 --- a/apis/app/node.api +++ /dev/null @@ -1,40 +0,0 @@ -syntax = "v1" - -info ( - title: "App Node Api" - desc: "API for ppanel" - author: "Tension" - email: "tension@ppanel.com" - version: "0.0.1" -) - -import "../types.api" - -type ( - AppRuleGroupListResponse { - Total int64 `json:"total"` - List []ServerRuleGroup `json:"list"` - } - AppUserSubscbribeNodeRequest { - Id int64 `form:"id" validate:"required"` - } - AppUserSubscbribeNodeResponse { - List []AppUserSubscbribeNode `json:"list"` - } -) - -@server ( - prefix: v1/app/node - group: app/node - middleware: AppMiddleware,AuthMiddleware -) -service ppanel { - @doc "Get Node list" - @handler GetNodeList - get /list (AppUserSubscbribeNodeRequest) returns (AppUserSubscbribeNodeResponse) - - @doc "Get rule group list" - @handler GetRuleGroupList - get /rule_group_list returns (AppRuleGroupListResponse) -} - diff --git a/apis/app/order.api b/apis/app/order.api deleted file mode 100644 index 301d62b..0000000 --- a/apis/app/order.api +++ /dev/null @@ -1,57 +0,0 @@ -syntax = "v1" - -info ( - title: "Order API" - desc: "API for ppanel" - author: "Tension" - email: "tension@ppanel.com" - version: "0.0.1" -) - -import ( - "../types.api" -) - -@server ( - prefix: v1/app/order - group: app/order - middleware: AppMiddleware,AuthMiddleware -) -service ppanel { - @doc "Pre create order" - @handler PreCreateOrder - post /pre (PurchaseOrderRequest) returns (PreOrderResponse) - - @doc "purchase Subscription" - @handler Purchase - post /purchase (PurchaseOrderRequest) returns (PurchaseOrderResponse) - - @doc "Renewal Subscription" - @handler Renewal - post /renewal (RenewalOrderRequest) returns (RenewalOrderResponse) - - @doc "Reset traffic" - @handler ResetTraffic - post /reset (ResetTrafficOrderRequest) returns (ResetTrafficOrderResponse) - - @doc "Recharge" - @handler Recharge - post /recharge (RechargeOrderRequest) returns (RechargeOrderResponse) - - @doc "Checkout order" - @handler CheckoutOrder - post /checkout (CheckoutOrderRequest) returns (CheckoutOrderResponse) - - @doc "Close order" - @handler CloseOrder - post /close (CloseOrderRequest) - - @doc "Get order" - @handler QueryOrderDetail - get /detail (QueryOrderDetailRequest) returns (OrderDetail) - - @doc "Get order list" - @handler QueryOrderList - get /list (QueryOrderListRequest) returns (QueryOrderListResponse) -} - diff --git a/apis/app/payment.api b/apis/app/payment.api deleted file mode 100644 index 9769a47..0000000 --- a/apis/app/payment.api +++ /dev/null @@ -1,23 +0,0 @@ -syntax = "v1" - -info ( - title: "payment API" - desc: "API for ppanel" - author: "Tension" - email: "tension@ppanel.com" - version: "0.0.1" -) - -import "../types.api" - -@server ( - prefix: v1/app/payment - group: app/payment - middleware: AppMiddleware,AuthMiddleware -) -service ppanel { - @doc "Get available payment methods" - @handler GetAvailablePaymentMethods - get /methods returns (GetAvailablePaymentMethodsResponse) -} - diff --git a/apis/app/subscribe.api b/apis/app/subscribe.api deleted file mode 100644 index 8ee9665..0000000 --- a/apis/app/subscribe.api +++ /dev/null @@ -1,65 +0,0 @@ -syntax = "v1" - -info ( - title: "Subscribe API" - desc: "API for ppanel" - author: "Tension" - email: "tension@ppanel.com" - version: "0.0.1" -) - -import "../types.api" - -type ( - QueryUserSubscribeResp { - Data []UserSubscribeData `json:"data"` - } - UserSubscribeData { - SubscribeId int64 `json:"subscribe_id"` - UserSubscribeId int64 `json:"user_subscribe_id"` - } - UserSubscribeResetPeriodRequest { - UserSubscribeId int64 `json:"user_subscribe_id"` - } - UserSubscribeResetPeriodResponse { - Status bool `json:"status"` - } - AppUserSubscribeRequest { - ContainsNodes *bool `form:"contains_nodes"` - } - AppUserSubscbribeResponse { - List []AppUserSubcbribe `json:"list"` - } -) - -@server ( - prefix: v1/app/subscribe - group: app/subscribe - middleware: AppMiddleware,AuthMiddleware -) -service ppanel { - @doc "Get subscribe list" - @handler QuerySubscribeList - get /list returns (QuerySubscribeListResponse) - - @doc "Get subscribe group list" - @handler QuerySubscribeGroupList - get /group/list returns (QuerySubscribeGroupListResponse) - - @doc "Get application config" - @handler QueryApplicationConfig - get /application/config returns (ApplicationResponse) - - @doc "Get Already subscribed to package" - @handler QueryUserAlreadySubscribe - get /user/already_subscribe returns (QueryUserSubscribeResp) - - @doc "Get Available subscriptions for users" - @handler QueryUserAvailableUserSubscribe - get /user/available_subscribe (AppUserSubscribeRequest) returns (AppUserSubscbribeResponse) - - @doc "Reset user subscription period" - @handler ResetUserSubscribePeriod - post /reset/period (UserSubscribeResetPeriodRequest) returns (UserSubscribeResetPeriodResponse) -} - diff --git a/apis/app/user.api b/apis/app/user.api deleted file mode 100644 index 953974a..0000000 --- a/apis/app/user.api +++ /dev/null @@ -1,86 +0,0 @@ -syntax = "v1" - -info ( - title: "App User Api" - desc: "API for ppanel" - author: "Tension" - email: "tension@ppanel.com" - version: "0.0.1" -) - -import ( - "../types.api" -) - -type ( - UserInfoResponse { - Id int64 `json:"id"` - Balance int64 `json:"balance"` - Email string `json:"email"` - RefererId int64 `json:"referer_id"` - ReferCode string `json:"refer_code"` - Avatar string `json:"avatar"` - AreaCode string `json:"area_code"` - Telephone string `json:"telephone"` - Devices []UserDevice `json:"devices"` - AuthMethods []UserAuthMethod `json:"auth_methods"` - } - UpdatePasswordRequeset { - Password string `json:"password"` - NewPassword string `json:"new_password"` - } - DeleteAccountRequest { - Method string `json:"method" validate:"required" validate:"required,oneof=email telephone device"` - Code string `json:"code"` - } - GetUserOnlineTimeStatisticsResponse { - WeeklyStats []WeeklyStat `json:"weekly_stats"` - ConnectionRecords ConnectionRecords `json:"connection_records"` - } - WeeklyStat { - Day int `json:"day"` - DayName string `json:"day_name"` - Hours float64 `json:"hours"` - } - ConnectionRecords { - CurrentContinuousDays int64 `json:"current_continuous_days"` - HistoryContinuousDays int64 `json:"history_continuous_days"` - LongestSingleConnection int64 `json:"longest_single_connection"` - } -) - -@server ( - prefix: v1/app/user - group: app/user - middleware: AppMiddleware,AuthMiddleware -) -service ppanel { - @doc "query user info" - @handler QueryUserInfo - get /info returns (UserInfoResponse) - - @doc "Update Password " - @handler UpdatePassword - put /password (UpdatePasswordRequeset) - - @doc "Delete Account" - @handler DeleteAccount - delete /account (DeleteAccountRequest) - - @doc "Get user subcribe traffic logs" - @handler GetUserSubscribeTrafficLogs - get /subscribe/traffic_logs (GetUserSubscribeTrafficLogsRequest) returns (GetUserSubscribeTrafficLogsResponse) - - @doc "Get user online time total" - @handler GetUserOnlineTimeStatistics - get /online_time/statistics returns (GetUserOnlineTimeStatisticsResponse) - - @doc "Query User Affiliate List" - @handler QueryUserAffiliateList - get /affiliate/list (QueryUserAffiliateListRequest) returns (QueryUserAffiliateListResponse) - - @doc "Query User Affiliate Count" - @handler QueryUserAffiliate - get /affiliate/count returns (QueryUserAffiliateCountResponse) -} - diff --git a/apis/app/ws.api b/apis/app/ws.api deleted file mode 100644 index f70b55e..0000000 --- a/apis/app/ws.api +++ /dev/null @@ -1,21 +0,0 @@ -syntax = "v1" - -info ( - title: "App Heartbeat Api" - desc: "API for ppanel" - author: "Tension" - email: "tension@ppanel.com" - version: "0.0.1" -) - -@server ( - prefix: v1/app/ws - group: app/ws - middleware: AuthMiddleware -) -service ppanel { - @doc "App heartbeat" - @handler AppWs - get /:userid/:identifier -} - diff --git a/apis/auth/auth.api b/apis/auth/auth.api index 50c82d1..8211bef 100644 --- a/apis/auth/auth.api +++ b/apis/auth/auth.api @@ -11,11 +11,13 @@ info ( type ( // User login request UserLoginRequest { - Email string `json:"email" validate:"required"` - Password string `json:"password" validate:"required"` - IP string `header:"X-Original-Forwarded-For"` - UserAgent string `header:"User-Agent"` - CfToken string `json:"cf_token,optional"` + Identifier string `json:"identifier"` + Email string `json:"email" validate:"required"` + Password string `json:"password" validate:"required"` + IP string `header:"X-Original-Forwarded-For"` + UserAgent string `header:"User-Agent"` + LoginType string `header:"Login-Type"` + CfToken string `json:"cf_token,optional"` } // Check user is exist request CheckUserRequest { @@ -27,22 +29,26 @@ type ( } // User login response UserRegisterRequest { - Email string `json:"email" validate:"required"` - Password string `json:"password" validate:"required"` - Invite string `json:"invite,optional"` - Code string `json:"code,optional"` - IP string `header:"X-Original-Forwarded-For"` - UserAgent string `header:"User-Agent"` - CfToken string `json:"cf_token,optional"` + Identifier string `json:"identifier"` + Email string `json:"email" validate:"required"` + Password string `json:"password" validate:"required"` + Invite string `json:"invite,optional"` + Code string `json:"code,optional"` + IP string `header:"X-Original-Forwarded-For"` + UserAgent string `header:"User-Agent"` + LoginType string `header:"Login-Type"` + CfToken string `json:"cf_token,optional"` } // User login response ResetPasswordRequest { - Email string `json:"email" validate:"required"` - Password string `json:"password" validate:"required"` - Code string `json:"code,optional"` - IP string `header:"X-Original-Forwarded-For"` - UserAgent string `header:"User-Agent"` - CfToken string `json:"cf_token,optional"` + Identifier string `json:"identifier"` + Email string `json:"email" validate:"required"` + Password string `json:"password" validate:"required"` + Code string `json:"code,optional"` + IP string `header:"X-Original-Forwarded-For"` + UserAgent string `header:"User-Agent"` + LoginType string `header:"Login-Type"` + CfToken string `json:"cf_token,optional"` } LoginResponse { Token string `json:"token"` @@ -60,11 +66,14 @@ type ( } // login request TelephoneLoginRequest { + Identifier string `json:"identifier"` Telephone string `json:"telephone" validate:"required"` TelephoneCode string `json:"telephone_code"` TelephoneAreaCode string `json:"telephone_area_code" validate:"required"` Password string `json:"password"` IP string `header:"X-Original-Forwarded-For"` + UserAgent string `header:"User-Agent"` + LoginType string `header:"Login-Type"` CfToken string `json:"cf_token,optional"` } // Check user is exist request @@ -78,21 +87,27 @@ type ( } // User login response TelephoneRegisterRequest { + Identifier string `json:"identifier"` Telephone string `json:"telephone" validate:"required"` TelephoneAreaCode string `json:"telephone_area_code" validate:"required"` Password string `json:"password" validate:"required"` Invite string `json:"invite,optional"` Code string `json:"code,optional"` IP string `header:"X-Original-Forwarded-For"` + UserAgent string `header:"User-Agent"` + LoginType string `header:"Login-Type,optional"` CfToken string `json:"cf_token,optional"` } // User login response TelephoneResetPasswordRequest { + Identifier string `json:"identifier"` Telephone string `json:"telephone" validate:"required"` TelephoneAreaCode string `json:"telephone_area_code" validate:"required"` Password string `json:"password" validate:"required"` Code string `json:"code,optional"` IP string `header:"X-Original-Forwarded-For"` + UserAgent string `header:"User-Agent"` + LoginType string `header:"Login-Type,optional"` CfToken string `json:"cf_token,optional"` } AppleLoginCallbackRequest { @@ -104,11 +119,18 @@ type ( Code string `form:"code"` State string `form:"state"` } + DeviceLoginRequest { + Identifier string `json:"identifier" validate:"required"` + IP string `header:"X-Original-Forwarded-For"` + UserAgent string `json:"user_agent" validate:"required"` + CfToken string `json:"cf_token,optional"` + } ) @server ( - prefix: v1/auth - group: auth + prefix: v1/auth + group: auth + middleware: DeviceMiddleware ) service ppanel { @doc "User login" @@ -142,6 +164,10 @@ service ppanel { @doc "Reset password" @handler TelephoneResetPassword post /reset/telephone (TelephoneResetPasswordRequest) returns (LoginResponse) + + @doc "Device Login" + @handler DeviceLogin + post /login/device (DeviceLoginRequest) returns (LoginResponse) } @server ( diff --git a/apis/common.api b/apis/common.api index 8f1d9e5..db935f4 100644 --- a/apis/common.api +++ b/apis/common.api @@ -35,10 +35,6 @@ type ( GetTosResponse { TosContent string `json:"tos_content"` } - GetAppcationResponse { - Config ApplicationConfig `json:"config"` - Applications []ApplicationResponseInfo `json:"applications"` - } // GetCodeRequest Get code request SendCodeRequest { Email string `json:"email" validate:"required"` @@ -78,21 +74,36 @@ type ( CheckVerificationCodeRespone { Status bool `json:"status"` } + SubscribeClient { + Id int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Icon string `json:"icon,omitempty"` + Scheme string `json:"scheme,omitempty"` + IsDefault bool `json:"is_default"` + DownloadLink DownloadLink `json:"download_link,omitempty"` + } + GetSubscribeClientResponse { + Total int64 `json:"total"` + List []SubscribeClient `json:"list"` + } + HeartbeatResponse { + Status bool `json:"status"` + Message string `json:"message,omitempty"` + Timestamp int64 `json:"timestamp,omitempty"` + } ) @server ( - prefix: v1/common - group: common + prefix: v1/common + group: common + middleware: DeviceMiddleware ) service ppanel { @doc "Get global config" @handler GetGlobalConfig get /site/config returns (GetGlobalConfigResponse) - @doc "Get Tos Content" - @handler GetApplication - get /application returns (GetAppcationResponse) - @doc "Get Tos Content" @handler GetTos get /site/tos returns (GetTosResponse) @@ -120,5 +131,13 @@ service ppanel { @doc "Check verification code" @handler CheckVerificationCode post /check_verification_code (CheckVerificationCodeRequest) returns (CheckVerificationCodeRespone) + + @doc "Get Client" + @handler GetClient + get /client returns (GetSubscribeClientResponse) + + @doc "Heartbeat" + @handler Heartbeat + get /heartbeat returns (HeartbeatResponse) } diff --git a/apis/node/node.api b/apis/node/node.api index 156bf64..8dee713 100644 --- a/apis/node/node.api +++ b/apis/node/node.api @@ -11,6 +11,10 @@ info ( import "../types.api" type ( + OnlineUser { + SID int64 `json:"uid"` + IP string `json:"ip"` + } ShadowsocksProtocol { Port int `json:"port"` Method string `json:"method"` @@ -89,6 +93,20 @@ type ( ServerCommon Users []OnlineUser `json:"users"` } + QueryServerConfigRequest { + ServerID int64 `path:"server_id"` + SecretKey string `form:"secret_key"` + Protocols []string `form:"protocols,omitempty"` + } + QueryServerConfigResponse { + TrafficReportThreshold int64 `json:"traffic_report_threshold"` + IPStrategy string `json:"ip_strategy"` + DNS []NodeDNS `json:"dns"` + Block []string `json:"block"` + Outbound []NodeOutbound `json:"outbound"` + Protocols []Protocol `json:"protocols"` + Total int64 `json:"total"` + } ) @server ( @@ -118,3 +136,13 @@ service ppanel { post /online (OnlineUsersRequest) } +@server ( + prefix: v2/server + group: server +) +service ppanel { + @doc "Get Server Protocol Config" + @handler QueryServerProtocolConfig + get /:server_id (QueryServerConfigRequest) returns (QueryServerConfigResponse) +} + diff --git a/apis/public/announcement.api b/apis/public/announcement.api index 5afd09b..7122e4e 100644 --- a/apis/public/announcement.api +++ b/apis/public/announcement.api @@ -13,7 +13,7 @@ import "../types.api" @server ( prefix: v1/public/announcement group: public/announcement - middleware: AuthMiddleware + middleware: AuthMiddleware,DeviceMiddleware ) service ppanel { @doc "Query announcement" diff --git a/apis/public/document.api b/apis/public/document.api index 4a5e6f9..660bea6 100644 --- a/apis/public/document.api +++ b/apis/public/document.api @@ -13,7 +13,7 @@ import "../types.api" @server ( prefix: v1/public/document group: public/document - middleware: AuthMiddleware + middleware: AuthMiddleware,DeviceMiddleware ) service ppanel { @doc "Get document list" diff --git a/apis/public/order.api b/apis/public/order.api index 0db556f..4e83b0f 100644 --- a/apis/public/order.api +++ b/apis/public/order.api @@ -13,7 +13,7 @@ import "../types.api" @server ( prefix: v1/public/order group: public/order - middleware: AuthMiddleware + middleware: AuthMiddleware,DeviceMiddleware ) service ppanel { @doc "Pre create order" diff --git a/apis/public/payment.api b/apis/public/payment.api index 4876abd..a4893ab 100644 --- a/apis/public/payment.api +++ b/apis/public/payment.api @@ -13,7 +13,7 @@ import "../types.api" @server ( prefix: v1/public/payment group: public/payment - middleware: AuthMiddleware + middleware: AuthMiddleware,DeviceMiddleware ) service ppanel { @doc "Get available payment methods" diff --git a/apis/public/portal.api b/apis/public/portal.api index e900ed9..2d33861 100644 --- a/apis/public/portal.api +++ b/apis/public/portal.api @@ -25,6 +25,9 @@ type ( PortalPurchaseResponse { OrderNo string `json:"order_no"` } + GetSubscriptionRequest { + Language string `form:"language"` + } GetSubscriptionResponse { List []Subscribe `json:"list"` } @@ -65,8 +68,9 @@ type ( ) @server ( - prefix: v1/public/portal - group: public/portal + prefix: v1/public/portal + group: public/portal + middleware: DeviceMiddleware ) service ppanel { @doc "Get available payment methods" @@ -75,7 +79,7 @@ service ppanel { @doc "Get Subscription" @handler GetSubscription - get /subscribe returns (GetSubscriptionResponse) + get /subscribe (GetSubscriptionRequest) returns (GetSubscriptionResponse) @doc "Pre Purchase Order" @handler PrePurchaseOrder diff --git a/apis/public/subscribe.api b/apis/public/subscribe.api index aaffe59..25234ad 100644 --- a/apis/public/subscribe.api +++ b/apis/public/subscribe.api @@ -10,22 +10,58 @@ info ( import "../types.api" +type ( + QuerySubscribeListRequest { + Language string `form:"language"` + } + QueryUserSubscribeNodeListResponse { + List []UserSubscribeInfo `json:"list"` + } + UserSubscribeInfo { + Id int64 `json:"id"` + UserId int64 `json:"user_id"` + OrderId int64 `json:"order_id"` + SubscribeId int64 `json:"subscribe_id"` + StartTime int64 `json:"start_time"` + ExpireTime int64 `json:"expire_time"` + FinishedAt int64 `json:"finished_at"` + ResetTime int64 `json:"reset_time"` + Traffic int64 `json:"traffic"` + Download int64 `json:"download"` + Upload int64 `json:"upload"` + Token string `json:"token"` + Status uint8 `json:"status"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + IsTryOut bool `json:"is_try_out"` + Nodes []*UserSubscribeNodeInfo `json:"nodes"` + } + UserSubscribeNodeInfo { + Id int64 `json:"id"` + Name string `json:"name"` + Uuid string `json:"uuid"` + Protocol string `json:"protocol"` + Port uint16 `json:"port"` + Address string `json:"address"` + Tags []string `json:"tags"` + Country string `json:"country"` + City string `json:"city"` + CreatedAt int64 `json:"created_at"` + } +) + @server ( prefix: v1/public/subscribe group: public/subscribe - middleware: AuthMiddleware + middleware: AuthMiddleware,DeviceMiddleware ) service ppanel { @doc "Get subscribe list" @handler QuerySubscribeList - get /list returns (QuerySubscribeListResponse) + get /list (QuerySubscribeListRequest) returns (QuerySubscribeListResponse) - @doc "Get subscribe group list" - @handler QuerySubscribeGroupList - get /group/list returns (QuerySubscribeGroupListResponse) - - @doc "Get application config" - @handler QueryApplicationConfig - get /application/config returns (ApplicationResponse) + @doc "Get user subscribe node info" + @handler QueryUserSubscribeNodeList + get /node/list returns (QueryUserSubscribeNodeListResponse) } diff --git a/apis/public/ticket.api b/apis/public/ticket.api index 0f39304..69bff62 100644 --- a/apis/public/ticket.api +++ b/apis/public/ticket.api @@ -43,7 +43,7 @@ type ( @server ( prefix: v1/public/ticket group: public/ticket - middleware: AuthMiddleware + middleware: AuthMiddleware,DeviceMiddleware ) service ppanel { @doc "Get ticket list" diff --git a/apis/public/user.api b/apis/public/user.api index 66c8d08..f37eef3 100644 --- a/apis/public/user.api +++ b/apis/public/user.api @@ -25,15 +25,8 @@ type ( Total int64 `json:"total"` } QueryUserBalanceLogListResponse { - List []UserBalanceLog `json:"list"` - Total int64 `json:"total"` - } - CommissionLog { - Id int64 `json:"id"` - UserId int64 `json:"user_id"` - OrderNo string `json:"order_no"` - Amount int64 `json:"amount"` - CreatedAt int64 `json:"created_at"` + List []BalanceLog `json:"list"` + Total int64 `json:"total"` } QueryUserCommissionLogListRequest { Page int `form:"page"` @@ -104,12 +97,48 @@ type ( Email string `json:"email" validate:"required"` Code string `json:"code" validate:"required"` } + GetDeviceListResponse { + List []UserDevice `json:"list"` + Total int64 `json:"total"` + } + UnbindDeviceRequest { + Id int64 `json:"id" validate:"required"` + } + UpdateUserSubscribeNoteRequest { + UserSubscribeId int64 `json:"user_subscribe_id" validate:"required"` + Note string `json:"note" validate:"max=500"` + } + UpdateUserRulesRequest { + Rules []string `json:"rules" validate:"required"` + } + CommissionWithdrawRequest { + Amount int64 `json:"amount"` + Content string `json:"content"` + } + WithdrawalLog { + Id int64 `json:"id"` + UserId int64 `json:"user_id"` + Amount int64 `json:"amount"` + Content string `json:"content"` + Status uint8 `json:"status"` + Reason string `json:"reason,omitempty"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + } + QueryWithdrawalLogListRequest { + Page int `form:"page"` + Size int `form:"size"` + } + QueryWithdrawalLogListResponse { + List []WithdrawalLog `json:"list"` + Total int64 `json:"total"` + } ) @server ( prefix: v1/public/user group: public/user - middleware: AuthMiddleware + middleware: AuthMiddleware,DeviceMiddleware ) service ppanel { @doc "Query User Info" @@ -199,5 +228,29 @@ service ppanel { @doc "Update Bind Email" @handler UpdateBindEmail put /bind_email (UpdateBindEmailRequest) + + @doc "Get Device List" + @handler GetDeviceList + get /devices returns (GetDeviceListResponse) + + @doc "Unbind Device" + @handler UnbindDevice + put /unbind_device (UnbindDeviceRequest) + + @doc "Update User Subscribe Note" + @handler UpdateUserSubscribeNote + put /subscribe_note (UpdateUserSubscribeNoteRequest) + + @doc "Update User Rules" + @handler UpdateUserRules + put /rules (UpdateUserRulesRequest) + + @doc "Commission Withdraw" + @handler CommissionWithdraw + post /commission_withdraw (CommissionWithdrawRequest) returns (WithdrawalLog) + + @doc "Query Withdrawal Log" + @handler QueryWithdrawalLog + get /withdrawal_log (QueryWithdrawalLogListRequest) returns (QueryWithdrawalLogListResponse) } diff --git a/apis/swagger_admin.api b/apis/swagger_admin.api index 791448a..e61a903 100644 --- a/apis/swagger_admin.api +++ b/apis/swagger_admin.api @@ -24,5 +24,7 @@ import ( "./admin/auth.api" "./admin/log.api" "./admin/ads.api" + "./admin/marketing.api" + "./admin/application.api" ) diff --git a/apis/swagger_app.api b/apis/swagger_app.api deleted file mode 100644 index 590b5bf..0000000 --- a/apis/swagger_app.api +++ /dev/null @@ -1,22 +0,0 @@ -syntax = "v1" - -info ( - title: "App API" - desc: "API for ppanel" - author: "Tension" - email: "tension@ppanel.com" - version: "0.0.1" -) - -import ( - "./app/auth.api" - "./app/user.api" - "./app/node.api" - "./app/ws.api" - "./app/order.api" - "./app/announcement.api" - "./app/payment.api" - "./app/document.api" - "./app/subscribe.api" -) - diff --git a/apis/types.api b/apis/types.api index 8773ba5..c6777c2 100644 --- a/apis/types.api +++ b/apis/types.api @@ -14,6 +14,8 @@ type ( Avatar string `json:"avatar"` Balance int64 `json:"balance"` Commission int64 `json:"commission"` + ReferralPercentage uint8 `json:"referral_percentage"` + OnlyFirstPurchase bool `json:"only_first_purchase"` GiftAmount int64 `json:"gift_amount"` Telegram int64 `json:"telegram"` ReferCode string `json:"refer_code"` @@ -26,6 +28,7 @@ type ( EnableTradeNotify bool `json:"enable_trade_notify"` AuthMethods []UserAuthMethod `json:"auth_methods"` UserDevices []UserDevice `json:"user_devices"` + Rules []string `json:"rules"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` DeletedAt int64 `json:"deleted_at,omitempty"` @@ -63,6 +66,8 @@ type ( SubscribePath string `json:"subscribe_path"` SubscribeDomain string `json:"subscribe_domain"` PanDomain bool `json:"pan_domain"` + UserAgentLimit bool `json:"user_agent_limit"` + UserAgentList string `json:"user_agent_list"` } VerifyCodeConfig { VerifyCodeExpireTime int64 `json:"verify_code_expire_time"` @@ -111,6 +116,7 @@ type ( AuthConfig { Mobile MobileAuthenticateConfig `json:"mobile"` Email EmailAuthticateConfig `json:"email"` + Device DeviceAuthticateConfig `json:"device"` Register PubilcRegisterConfig `json:"register"` } PubilcRegisterConfig { @@ -130,6 +136,12 @@ type ( EnableDomainSuffix bool `json:"enable_domain_suffix"` DomainSuffixList string `json:"domain_suffix_list"` } + DeviceAuthticateConfig { + Enable bool `json:"enable"` + ShowAds bool `json:"show_ads"` + EnableSecurity bool `json:"enable_security"` + OnlyRealDevice bool `json:"only_real_device"` + } RegisterConfig { StopRegister bool `json:"stop_register"` EnableTrial bool `json:"enable_trial"` @@ -148,9 +160,27 @@ type ( EnableResetPasswordVerify bool `json:"enable_reset_password_verify"` } NodeConfig { - NodeSecret string `json:"node_secret"` - NodePullInterval int64 `json:"node_pull_interval"` - NodePushInterval int64 `json:"node_push_interval"` + NodeSecret string `json:"node_secret"` + NodePullInterval int64 `json:"node_pull_interval"` + NodePushInterval int64 `json:"node_push_interval"` + TrafficReportThreshold int64 `json:"traffic_report_threshold"` + IPStrategy string `json:"ip_strategy"` + DNS []NodeDNS `json:"dns"` + Block []string `json:"block"` + Outbound []NodeOutbound `json:"outbound"` + } + NodeDNS { + Proto string `json:"proto"` + Address string `json:"address"` + Domains []string `json:"domains"` + } + NodeOutbound { + Name string `json:"name"` + Protocol string `json:"protocol"` + Address string `json:"address"` + Port int64 `json:"port"` + Password string `json:"password"` + Rules []string `json:"rules"` } InviteConfig { ForcedInvite bool `json:"forced_invite"` @@ -182,6 +212,7 @@ type ( Subscribe { Id int64 `json:"id"` Name string `json:"name"` + Language string `json:"language"` Description string `json:"description"` UnitPrice int64 `json:"unit_price"` UnitTime string `json:"unit_time"` @@ -192,9 +223,8 @@ type ( SpeedLimit int64 `json:"speed_limit"` DeviceLimit int64 `json:"device_limit"` Quota int64 `json:"quota"` - GroupId int64 `json:"group_id"` - ServerGroup []int64 `json:"server_group"` - Server []int64 `json:"server"` + Nodes []int64 `json:"nodes"` + NodeTags []string `json:"node_tags"` Show bool `json:"show"` Sell bool `json:"sell"` Sort int64 `json:"sort"` @@ -273,37 +303,37 @@ type ( Host string `json:"host"` ServiceName string `json:"service_name"` } - Server { - Id int64 `json:"id"` - Tags []string `json:"tags"` - Country string `json:"country"` - City string `json:"city"` - Name string `json:"name"` - ServerAddr string `json:"server_addr"` - RelayMode string `json:"relay_mode"` - RelayNode []NodeRelay `json:"relay_node"` - SpeedLimit int `json:"speed_limit"` - TrafficRatio float32 `json:"traffic_ratio"` - GroupId int64 `json:"group_id"` - Protocol string `json:"protocol"` - Config interface{} `json:"config"` - Enable *bool `json:"enable"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` - Status *NodeStatus `json:"status"` - Sort int64 `json:"sort"` - } - OnlineUser { - SID int64 `json:"uid"` - IP string `json:"ip"` - } - NodeStatus { - Online interface{} `json:"online"` - Cpu float64 `json:"cpu"` - Mem float64 `json:"mem"` - Disk float64 `json:"disk"` - UpdatedAt int64 `json:"updated_at"` - } + // Server { + // Id int64 `json:"id"` + // Tags []string `json:"tags"` + // Country string `json:"country"` + // City string `json:"city"` + // Name string `json:"name"` + // ServerAddr string `json:"server_addr"` + // RelayMode string `json:"relay_mode"` + // RelayNode []NodeRelay `json:"relay_node"` + // SpeedLimit int `json:"speed_limit"` + // TrafficRatio float32 `json:"traffic_ratio"` + // GroupId int64 `json:"group_id"` + // Protocol string `json:"protocol"` + // Config interface{} `json:"config"` + // Enable *bool `json:"enable"` + // CreatedAt int64 `json:"created_at"` + // UpdatedAt int64 `json:"updated_at"` + // Status *NodeStatus `json:"status"` + // Sort int64 `json:"sort"` + // } + // OnlineUser { + // SID int64 `json:"uid"` + // IP string `json:"ip"` + // } + // NodeStatus { + // Online interface{} `json:"online"` + // Cpu float64 `json:"cpu"` + // Mem float64 `json:"mem"` + // Disk float64 `json:"disk"` + // UpdatedAt int64 `json:"updated_at"` + // } ServerGroup { Id int64 `json:"id"` Name string `json:"name"` @@ -442,18 +472,10 @@ type ( Upload int64 `json:"upload"` Token string `json:"token"` Status uint8 `json:"status"` + Short string `json:"short"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` } - UserBalanceLog { - Id int64 `json:"id"` - UserId int64 `json:"user_id"` - Amount int64 `json:"amount"` - Type uint8 `json:"type"` - OrderId int64 `json:"order_id"` - Balance int64 `json:"balance"` - CreatedAt int64 `json:"created_at"` - } UserAffiliate { Avatar string `json:"avatar"` Identifier string `json:"identifier"` @@ -474,14 +496,6 @@ type ( Port int `json:"port"` Prefix string `json:"prefix"` } - ApplicationConfig { - AppId int64 `json:"app_id"` - EncryptionKey string `json:"encryption_key"` - EncryptionMethod string `json:"encryption_method"` - Domains []string `json:"domains" validate:"required"` - StartupPicture string `json:"startup_picture"` - StartupPictureSkipTime int64 `json:"startup_picture_skip_time"` - } UserDevice { Id int64 `json:"id"` Ip string `json:"ip"` @@ -516,7 +530,7 @@ type ( Id int64 `json:"id"` Icon string `json:"icon"` Name string `json:"name" validate:"required"` - Type string `json:"type"` + Type string `json:"type"` Tags []string `json:"tags"` Rules string `json:"rules"` Enable bool `json:"enable"` @@ -531,7 +545,7 @@ type ( Token string `json:"token"` IP string `json:"ip"` UserAgent string `json:"user_agent"` - CreatedAt int64 `json:"created_at"` + Timestamp int64 `json:"timestamp"` } UserLoginLog { Id int64 `json:"id"` @@ -539,18 +553,17 @@ type ( LoginIP string `json:"login_ip"` UserAgent string `json:"user_agent"` Success bool `json:"success"` - CreatedAt int64 `json:"created_at"` + Timestamp int64 `json:"timestamp"` } MessageLog { - Id int64 `json:"id"` - Type string `json:"type"` - Platform string `json:"platform"` - To string `json:"to"` - Subject string `json:"subject"` - Content string `json:"content"` - Status int `json:"status"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` + Id int64 `json:"id"` + Type uint8 `json:"type"` + Platform string `json:"platform"` + To string `json:"to"` + Subject string `json:"subject"` + Content interface{} `json:"content"` + Status uint8 `json:"status"` + CreatedAt int64 `json:"created_at"` } Ads { Id int `json:"id"` @@ -754,5 +767,84 @@ type ( CreatedAt int64 `json:"created_at"` Download int64 `json:"download"` } + DownloadLink { + IOS string `json:"ios,omitempty"` + Android string `json:"android,omitempty"` + Windows string `json:"windows,omitempty"` + Mac string `json:"mac,omitempty"` + Linux string `json:"linux,omitempty"` + Harmony string `json:"harmony,omitempty"` + } + ResetSubscribeTrafficLog { + Id int64 `json:"id"` + Type uint16 `json:"type"` + UserSubscribeId int64 `json:"user_subscribe_id"` + OrderNo string `json:"order_no,omitempty"` + Timestamp int64 `json:"timestamp"` + } + BalanceLog { + Type uint16 `json:"type"` + UserId int64 `json:"user_id"` + Amount int64 `json:"amount"` + OrderNo string `json:"order_no,omitempty"` + Balance int64 `json:"balance"` + Timestamp int64 `json:"timestamp"` + } + CommissionLog { + Type uint16 `json:"type"` + UserId int64 `json:"user_id"` + Amount int64 `json:"amount"` + OrderNo string `json:"order_no"` + Timestamp int64 `json:"timestamp"` + } + Protocol { + Type string `json:"type"` + Port uint16 `json:"port"` + Enable bool `json:"enable"` + Security string `json:"security,omitempty"` + SNI string `json:"sni,omitempty"` + AllowInsecure bool `json:"allow_insecure,omitempty"` + Fingerprint string `json:"fingerprint,omitempty"` + RealityServerAddr string `json:"reality_server_addr,omitempty"` + RealityServerPort int `json:"reality_server_port,omitempty"` + RealityPrivateKey string `json:"reality_private_key,omitempty"` + RealityPublicKey string `json:"reality_public_key,omitempty"` + RealityShortId string `json:"reality_short_id,omitempty"` + Transport string `json:"transport,omitempty"` + Host string `json:"host,omitempty"` + Path string `json:"path,omitempty"` + ServiceName string `json:"service_name,omitempty"` + Cipher string `json:"cipher,omitempty"` + ServerKey string `json:"server_key,omitempty"` + Flow string `json:"flow,omitempty"` + HopPorts string `json:"hop_ports,omitempty"` + HopInterval int `json:"hop_interval,omitempty"` + ObfsPassword string `json:"obfs_password,omitempty"` + DisableSNI bool `json:"disable_sni,omitempty"` + ReduceRtt bool `json:"reduce_rtt,omitempty"` + UDPRelayMode string `json:"udp_relay_mode,omitempty"` + CongestionController string `json:"congestion_controller,omitempty"` + Multiplex string `json:"multiplex,omitempty"` // mux, eg: off/low/medium/high + PaddingScheme string `json:"padding_scheme,omitempty"` // padding scheme + UpMbps int `json:"up_mbps,omitempty"` // upload speed limit + DownMbps int `json:"down_mbps,omitempty"` // download speed limit + Obfs string `json:"obfs,omitempty"` // obfs, 'none', 'http', 'tls' + ObfsHost string `json:"obfs_host,omitempty"` // obfs host + ObfsPath string `json:"obfs_path,omitempty"` // obfs path + XhttpMode string `json:"xhttp_mode,omitempty"` // xhttp mode + XhttpExtra string `json:"xhttp_extra,omitempty"` // xhttp extra path + Encryption string `json:"encryption,omitempty"` // encryption,'none', 'mlkem768x25519plus' + EncryptionMode string `json:"encryption_mode,omitempty"` // encryption mode,'native', 'xorpub', 'random' + EncryptionRtt string `json:"encryption_rtt,omitempty"` // encryption rtt,'0rtt', '1rtt' + EncryptionTicket string `json:"encryption_ticket,omitempty"` // encryption ticket + EncryptionServerPadding string `json:"encryption_server_padding,omitempty"` // encryption server padding + EncryptionPrivateKey string `json:"encryption_private_key,omitempty"` // encryption private key + EncryptionClientPadding string `json:"encryption_client_padding,omitempty"` // encryption client padding + EncryptionPassword string `json:"encryption_password,omitempty"` // encryption password + Ratio float64 `json:"ratio,omitempty"` // Traffic ratio, default is 1 + CertMode string `json:"cert_mode,omitempty"` // Certificate mode, `none`|`http`|`dns`|`self` + CertDNSProvider string `json:"cert_dns_provider,omitempty"` // DNS provider for certificate + CertDNSEnv string `json:"cert_dns_env,omitempty"` // Environment for DNS provider + } ) diff --git a/go.mod b/go.mod index c0145a7..9d0b191 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/go-sql-driver/mysql v1.8.1 github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 github.com/gofrs/uuid/v5 v5.3.0 - github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/golang-jwt/jwt/v5 v5.2.2 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 github.com/hibiken/asynq v0.24.1 @@ -28,7 +28,7 @@ require ( github.com/klauspost/compress v1.17.7 github.com/nyaruka/phonenumbers v1.5.0 github.com/pkg/errors v0.9.1 - github.com/redis/go-redis/v9 v9.6.1 + github.com/redis/go-redis/v9 v9.7.2 github.com/smartwalle/alipay/v3 v3.2.23 github.com/spf13/cast v1.7.0 // indirect github.com/spf13/cobra v1.8.1 @@ -44,7 +44,7 @@ require ( go.opentelemetry.io/otel/sdk v1.29.0 go.opentelemetry.io/otel/trace v1.29.0 go.uber.org/zap v1.27.0 - golang.org/x/crypto v0.32.0 + golang.org/x/crypto v0.35.0 golang.org/x/oauth2 v0.25.0 golang.org/x/time v0.6.0 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df @@ -60,6 +60,7 @@ require ( github.com/fatih/color v1.18.0 github.com/goccy/go-json v0.10.4 github.com/golang-migrate/migrate/v4 v4.18.2 + github.com/oschwald/geoip2-golang v1.13.0 github.com/spaolacci/murmur3 v1.1.0 google.golang.org/grpc v1.64.1 google.golang.org/protobuf v1.36.3 @@ -117,6 +118,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/openzipkin/zipkin-go v0.4.2 // indirect + github.com/oschwald/maxminddb-golang v1.13.0 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect @@ -138,8 +140,8 @@ require ( golang.org/x/arch v0.13.0 // indirect golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d // indirect golang.org/x/net v0.34.0 // indirect - golang.org/x/sys v0.29.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect diff --git a/go.sum b/go.sum index cc90adf..aa84f0e 100644 --- a/go.sum +++ b/go.sum @@ -155,8 +155,8 @@ github.com/gofrs/uuid/v5 v5.3.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= -github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= -github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-migrate/migrate/v4 v4.18.2 h1:2VSCMz7x7mjyTXx3m2zPokOY82LTRgxK1yQYKo6wWQ8= github.com/golang-migrate/migrate/v4 v4.18.2/go.mod h1:2CM6tJvn2kqPXwnXO/d3rAQYiyoIm180VsO8PRX6Rpk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -285,6 +285,10 @@ github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQ github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/openzipkin/zipkin-go v0.4.2 h1:zjqfqHjUpPmB3c1GlCvvgsM1G4LkvqQbBDueDOCg/jA= github.com/openzipkin/zipkin-go v0.4.2/go.mod h1:ZeVkFjuuBiSy13y8vpSDCjMi9GoI3hPpCJSBx/EYFhY= +github.com/oschwald/geoip2-golang v1.13.0 h1:Q44/Ldc703pasJeP5V9+aFSZFmBN7DKHbNsSFzQATJI= +github.com/oschwald/geoip2-golang v1.13.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo= +github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU= +github.com/oschwald/maxminddb-golang v1.13.0/go.mod h1:BU0z8BfFVhi1LQaonTwwGQlsHUEu9pWNdMfmq4ztm0o= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -294,8 +298,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/redis/go-redis/v9 v9.0.3/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= -github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= -github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= +github.com/redis/go-redis/v9 v9.7.2 h1:PSGhv13dJyrTCw1+55H0pIKM3WFov7HuUrKUmInGL0o= +github.com/redis/go-redis/v9 v9.7.2/go.mod h1:yp5+a5FnEEP0/zTYuw6u6/2nn3zivwhv274qYgWQhDM= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= @@ -402,8 +406,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= -golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= +golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d h1:N0hmiNbwsSNwHBAvR3QB5w25pUwH4tK0Y/RltD1j1h4= golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= @@ -463,8 +467,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -478,8 +482,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= diff --git a/initialize/config.go b/initialize/config.go index 025220f..0667543 100644 --- a/initialize/config.go +++ b/initialize/config.go @@ -9,6 +9,7 @@ import ( "net/http" "os" + "github.com/perfect-panel/server/internal/report" "github.com/perfect-panel/server/pkg/logger" "gorm.io/driver/mysql" @@ -35,10 +36,30 @@ func Config(path string) (chan bool, *http.Server) { configPath = path // Create a new Gin instance r := gin.Default() + // get server port + port := 8080 + host := "127.0.0.1" + // check gateway mode + if report.IsGatewayMode() { + // get free port + freePort, err := report.ModulePort() + if err != nil { + logger.Errorf("get module port error: %s", err.Error()) + panic(err) + } + port = freePort + // register module + err = report.RegisterModule(port) + if err != nil { + logger.Errorf("register module error: %s", err.Error()) + panic(err) + } + logger.Infof("module registered on port %d", port) + } // Create a new HTTP server server := &http.Server{ - Addr: ":8080", + Addr: fmt.Sprintf("%s:%d", host, port), Handler: r, } // Load templates diff --git a/initialize/device.go b/initialize/device.go new file mode 100644 index 0000000..1b8c527 --- /dev/null +++ b/initialize/device.go @@ -0,0 +1,26 @@ +package initialize + +import ( + "context" + + "github.com/perfect-panel/server/pkg/logger" + + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/model/auth" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/tool" +) + +func Device(ctx *svc.ServiceContext) { + logger.Debug("device config initialization") + method, err := ctx.AuthModel.FindOneByMethod(context.Background(), "device") + if err != nil { + panic(err) + } + var cfg config.DeviceConfig + var deviceConfig auth.DeviceConfig + deviceConfig.Unmarshal(method.Config) + tool.DeepCopy(&cfg, deviceConfig) + cfg.Enable = *method.Enabled + ctx.Config.Device = cfg +} diff --git a/initialize/init.go b/initialize/init.go index bad262f..8023ce5 100644 --- a/initialize/init.go +++ b/initialize/init.go @@ -9,12 +9,12 @@ func StartInitSystemConfig(svc *svc.ServiceContext) { Site(svc) Node(svc) Email(svc) + Device(svc) Invite(svc) Verify(svc) Subscribe(svc) Register(svc) Mobile(svc) - TrafficDataToRedis(svc) if !svc.Config.Debug { Telegram(svc) } diff --git a/initialize/migrate/database/00001_init_schema.up.sql b/initialize/migrate/database/00001_init_schema.up.sql index c554874..9be1ddb 100644 --- a/initialize/migrate/database/00001_init_schema.up.sql +++ b/initialize/migrate/database/00001_init_schema.up.sql @@ -91,7 +91,6 @@ CREATE TABLE IF NOT EXISTS `auth_method` PRIMARY KEY (`id`), UNIQUE KEY `uni_auth_method` (`method`) ) ENGINE = InnoDB - AUTO_INCREMENT = 9 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci; @@ -305,7 +304,6 @@ CREATE TABLE IF NOT EXISTS `subscribe_type` `updated_at` datetime(3) DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`id`) ) ENGINE = InnoDB - AUTO_INCREMENT = 15 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci; @@ -323,7 +321,6 @@ CREATE TABLE IF NOT EXISTS `system` UNIQUE KEY `uni_system_key` (`key`), KEY `index_key` (`key`) ) ENGINE = InnoDB - AUTO_INCREMENT = 42 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci; @@ -398,7 +395,6 @@ CREATE TABLE IF NOT EXISTS `user` PRIMARY KEY (`id`), KEY `idx_referer` (`referer_id`) ) ENGINE = InnoDB - AUTO_INCREMENT = 2 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci; @@ -415,7 +411,6 @@ CREATE TABLE IF NOT EXISTS `user_auth_methods` UNIQUE KEY `idx_auth_identifier` (`auth_identifier`), KEY `idx_user_id` (`user_id`) ) ENGINE = InnoDB - AUTO_INCREMENT = 2 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci; diff --git a/initialize/migrate/database/02100_task.down.sql b/initialize/migrate/database/02100_task.down.sql new file mode 100644 index 0000000..0d1f763 --- /dev/null +++ b/initialize/migrate/database/02100_task.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS `email_task`; \ No newline at end of file diff --git a/initialize/migrate/database/02100_task.up.sql b/initialize/migrate/database/02100_task.up.sql new file mode 100644 index 0000000..d35e8c9 --- /dev/null +++ b/initialize/migrate/database/02100_task.up.sql @@ -0,0 +1,23 @@ +DROP TABLE IF EXISTS `email_task`; +CREATE TABLE `email_task` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID', + `subject` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Email Subject', + `content` text COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Email Content', + `recipient` text COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Email Recipient', + `scope` varchar(50) COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Email Scope', + `register_start_time` datetime(3) DEFAULT NULL COMMENT 'Register Start Time', + `register_end_time` datetime(3) DEFAULT NULL COMMENT 'Register End Time', + `additional` text COLLATE utf8mb4_general_ci COMMENT 'Additional Information', + `scheduled` datetime(3) NOT NULL COMMENT 'Scheduled Time', + `interval` tinyint unsigned NOT NULL COMMENT 'Interval in Seconds', + `limit` bigint unsigned NOT NULL COMMENT 'Daily send limit', + `status` tinyint unsigned NOT NULL COMMENT 'Daily Status', + `errors` text COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Errors', + `total` bigint unsigned NOT NULL DEFAULT '0' COMMENT 'Total Number', + `current` bigint unsigned NOT NULL DEFAULT '0' COMMENT 'Current Number', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/initialize/migrate/database/02101_subscribe_application.down.sql b/initialize/migrate/database/02101_subscribe_application.down.sql new file mode 100644 index 0000000..bca110a --- /dev/null +++ b/initialize/migrate/database/02101_subscribe_application.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS `subscribe_application`; \ No newline at end of file diff --git a/initialize/migrate/database/02101_subscribe_application.up.sql b/initialize/migrate/database/02101_subscribe_application.up.sql new file mode 100644 index 0000000..c46bc2b --- /dev/null +++ b/initialize/migrate/database/02101_subscribe_application.up.sql @@ -0,0 +1,27 @@ +DROP TABLE IF EXISTS `subscribe_application`; +CREATE TABLE IF NOT EXISTS `subscribe_application` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Application Name', + `icon` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Application Icon', + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Application Description', + `scheme` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Application Scheme', + `user_agent` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'User Agent', + `is_default` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Is Default Application', + `subscribe_template` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Subscribe Template', + `output_format` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'yaml' COMMENT 'Output Format', + `download_link` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Download Link', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- ---------------------------- +-- Records of subscribe_application +-- ---------------------------- +BEGIN; +INSERT INTO `subscribe_application` (`id`, `name`, `icon`, `description`, `scheme`, `user_agent`, `is_default`, `subscribe_template`, `output_format`, `download_link`, `created_at`, `updated_at`) VALUES (1, 'Default', '', '', '', 'default', 1, '{{- $GiB := 1073741824.0 -}}\n{{- $used := printf \"%.2f\" (divf (add (.UserInfo.Download | default 0 | float64) (.UserInfo.Upload | default 0 | float64)) $GiB) -}}\n{{- $traffic := (.UserInfo.Traffic | default 0 | float64) -}}\n{{- $total := printf \"%.2f\" (divf $traffic $GiB) -}}\n\n{{- $exp := \"\" -}}\n{{- $expStr := printf \"%v\" .UserInfo.ExpiredAt -}}\n{{- if regexMatch `^[0-9]+$` $expStr -}}\n {{- $ts := $expStr | float64 -}}\n {{- $sec := ternary (divf $ts 1000.0) $ts (ge (len $expStr) 13) -}}\n {{- $exp = (date \"2006-01-02 15:04:05\" (unixEpoch ($sec | int64))) -}}\n{{- else -}}\n {{- $exp = $expStr -}}\n{{- end -}}\n\nREMARKS={{ .SiteName }}-{{ .SubscribeName }}\nSTATUS=Traffic: {{ $used }} GiB/{{ $total }} GiB | Expires: {{ $exp }}\n\n{{- range $proxy := .Proxies }}\n {{- $server := $proxy.Server -}}\n {{- if and (contains $proxy.Server \":\") (not (hasPrefix \"[\" $proxy.Server)) -}}\n {{- $server = printf \"[%s]\" $proxy.Server -}}\n {{- end -}}\n\n {{- $sni := default \"\" $proxy.SNI -}}\n {{- if eq $sni \"\" -}}\n {{- $sni = default \"\" $proxy.Host -}}\n {{- end -}}\n {{- if and (eq $sni \"\") (not (or (regexMatch \"^[0-9]+\\\\.[0-9]+\\\\.[0-9]+\\\\.[0-9]+$\" $proxy.Server) (contains $proxy.Server \":\"))) -}}\n {{- $sni = $proxy.Server -}}\n {{- end -}}\n\n {{- $common := \"udp=1&tfo=1\" -}}\n\n {{- $password := $.UserInfo.Password -}}\n {{- if and (eq $proxy.Type \"shadowsocks\") (ne (default \"\" $proxy.ServerKey) \"\") -}}\n {{- $method := $proxy.Method -}}\n {{- if or (hasPrefix \"2022-blake3-\" $method) (eq $method \"2022-blake3-aes-128-gcm\") (eq $method \"2022-blake3-aes-256-gcm\") -}}\n {{- $userKeyLen := ternary 16 32 (hasSuffix \"128-gcm\" $method) -}}\n {{- $pwdStr := printf \"%s\" $password -}}\n {{- $userKey := ternary $pwdStr (trunc $userKeyLen $pwdStr) (le (len $pwdStr) $userKeyLen) -}}\n {{- $serverB64 := b64enc $proxy.ServerKey -}}\n {{- $userB64 := b64enc $userKey -}}\n {{- $password = printf \"%s:%s\" $serverB64 $userB64 -}}\n {{- end -}}\n {{- end -}}\n\n {{- if eq $proxy.Type \"shadowsocks\" }}\nss://{{ printf \"%s:%s\" $proxy.Method $password | b64enc }}@{{ $server }}:{{ $proxy.Port }}?{{ $common }}#{{ $proxy.Name }}\n {{- else if eq $proxy.Type \"vmess\" }}\nvmess://{{ (dict \"v\" \"2\" \"ps\" $proxy.Name \"add\" $proxy.Server \"port\" (printf \"%d\" $proxy.Port) \"id\" $password \"aid\" \"0\" \"net\" (default \"tcp\" $proxy.Transport) \"type\" \"none\" \"host\" (default \"\" $proxy.Host) \"path\" (default \"\" $proxy.Path) \"tls\" (ternary \"tls\" \"\" (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\"))) \"sni\" $sni) | toJson | b64enc }}\n {{- else if eq $proxy.Type \"vless\" }}\nvless://{{ $password }}@{{ $server }}:{{ $proxy.Port }}?encryption=none{{- if ne (default \"\" $proxy.Flow) \"\" }}&flow={{ $proxy.Flow }}{{- end }}{{- if ne $proxy.Transport \"\" }}&type={{ $proxy.Transport }}{{- end }}{{- if and (eq $proxy.Transport \"ws\") (ne (default \"\" $proxy.Host) \"\") }}&host={{ $proxy.Host }}{{- end }}{{- if and (eq $proxy.Transport \"ws\") (ne (default \"\" $proxy.Path) \"\") }}&path={{ $proxy.Path | urlquery }}{{- end }}{{- if and (eq $proxy.Transport \"grpc\") (ne (default \"\" $proxy.ServiceName) \"\") }}&serviceName={{ $proxy.ServiceName }}{{- end }}{{- if or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\") }}&security={{ $proxy.Security }}{{- end }}{{- if ne $sni \"\" }}&sni={{ $sni }}{{- end }}{{- if $proxy.AllowInsecure }}&allowInsecure=1{{- end }}{{- if ne (default \"\" $proxy.Fingerprint) \"\" }}&fp={{ $proxy.Fingerprint }}{{- end }}{{- if and (eq $proxy.Security \"reality\") (ne (default \"\" $proxy.RealityPublicKey) \"\") }}&pbk={{ $proxy.RealityPublicKey }}{{- end }}{{- if and (eq $proxy.Security \"reality\") (ne (default \"\" $proxy.RealityShortId) \"\") }}&sid={{ $proxy.RealityShortId }}{{- end }}&{{ $common }}#{{ $proxy.Name }}\n {{- else if eq $proxy.Type \"trojan\" }}\ntrojan://{{ $password }}@{{ $server }}:{{ $proxy.Port }}{{- if or (and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) (ne $sni \"\")) (and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) $proxy.AllowInsecure) (ne $proxy.Transport \"\") }}?{{- end }}{{- if and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) (ne $sni \"\") }}sni={{ $sni }}{{- end }}{{- if and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) $proxy.AllowInsecure }}{{- if and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) (ne $sni \"\") }}&{{- end }}allowInsecure=1{{- end }}{{- if ne $proxy.Transport \"\" }}{{- if or (and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) (ne $sni \"\")) (and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) $proxy.AllowInsecure) }}&{{- end }}type={{ $proxy.Transport }}{{- end }}{{- if and (eq $proxy.Transport \"ws\") (ne (default \"\" $proxy.Host) \"\") }}{{- if or (and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) (ne $sni \"\")) (and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) $proxy.AllowInsecure) (ne $proxy.Transport \"\") }}&{{- end }}host={{ $proxy.Host }}{{- end }}{{- if and (eq $proxy.Transport \"ws\") (ne (default \"\" $proxy.Path) \"\") }}{{- if or (and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) (ne $sni \"\")) (and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) $proxy.AllowInsecure) (ne $proxy.Transport \"\") (and (eq $proxy.Transport \"ws\") (ne (default \"\" $proxy.Host) \"\")) }}&{{- end }}path={{ $proxy.Path | urlquery }}{{- end }}{{- if or (and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) (ne $sni \"\")) (and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) $proxy.AllowInsecure) (ne $proxy.Transport \"\") }}&{{ $common }}{{- else }}?{{ $common }}{{- end }}#{{ $proxy.Name }}\n {{- else if eq $proxy.Type \"hysteria2\" }}\nhysteria2://{{ $server }}:{{ $proxy.Port }}{{- if or (ne $password \"\") (ne $sni \"\") $proxy.AllowInsecure (ne (default \"\" $proxy.ObfsPassword) \"\") (ne (default \"\" $proxy.HopPorts) \"\") }}?{{- end }}{{- if ne $password \"\" }}auth={{ $password }}{{- end }}{{- if ne $sni \"\" }}{{- if ne $password \"\" }}&{{- end }}sni={{ $sni }}{{- end }}{{- if $proxy.AllowInsecure }}{{- if or (ne $password \"\") (ne $sni \"\") }}&{{- end }}insecure=1{{- end }}{{- if ne (default \"\" $proxy.ObfsPassword) \"\" }}{{- if or (ne $password \"\") (ne $sni \"\") $proxy.AllowInsecure }}&{{- end }}obfs=salamander&obfs-password={{ $proxy.ObfsPassword }}{{- end }}{{- if ne (default \"\" $proxy.HopPorts) \"\" }}{{- if or (ne $password \"\") (ne $sni \"\") $proxy.AllowInsecure (ne (default \"\" $proxy.ObfsPassword) \"\") }}&{{- end }}mport={{ $proxy.HopPorts }}{{- end }}{{- if or (ne $password \"\") (ne $sni \"\") $proxy.AllowInsecure (ne (default \"\" $proxy.ObfsPassword) \"\") (ne (default \"\" $proxy.HopPorts) \"\") }}&{{ $common }}{{- else }}?{{ $common }}{{- end }}#{{ $proxy.Name }}\n {{- else if eq $proxy.Type \"tuic\" }}\ntuic://{{ $password }}:{{ $password }}@{{ $server }}:{{ $proxy.Port }}{{- if or (ne (default \"\" $proxy.CongestionController) \"\") (ne (default \"\" $proxy.UDPRelayMode) \"\") $proxy.ReduceRtt $proxy.DisableSNI (ne $sni \"\") $proxy.AllowInsecure }}?{{- end }}{{- if ne (default \"\" $proxy.CongestionController) \"\" }}congestion_controller={{ $proxy.CongestionController }}{{- end }}{{- if ne (default \"\" $proxy.UDPRelayMode) \"\" }}{{- if ne (default \"\" $proxy.CongestionController) \"\" }}&{{- end }}udp_relay_mode={{ $proxy.UDPRelayMode }}{{- end }}{{- if $proxy.ReduceRtt }}{{- if or (ne (default \"\" $proxy.CongestionController) \"\") (ne (default \"\" $proxy.UDPRelayMode) \"\") }}&{{- end }}reduce_rtt=1{{- end }}{{- if $proxy.DisableSNI }}{{- if or (ne (default \"\" $proxy.CongestionController) \"\") (ne (default \"\" $proxy.UDPRelayMode) \"\") $proxy.ReduceRtt }}&{{- end }}disable_sni=1{{- end }}{{- if ne $sni \"\" }}{{- if or (ne (default \"\" $proxy.CongestionController) \"\") (ne (default \"\" $proxy.UDPRelayMode) \"\") $proxy.ReduceRtt $proxy.DisableSNI }}&{{- end }}sni={{ $sni }}{{- end }}{{- if $proxy.AllowInsecure }}{{- if or (ne (default \"\" $proxy.CongestionController) \"\") (ne (default \"\" $proxy.UDPRelayMode) \"\") $proxy.ReduceRtt $proxy.DisableSNI (ne $sni \"\") }}&{{- end }}allow_insecure=1{{- end }}{{- if or (ne (default \"\" $proxy.CongestionController) \"\") (ne (default \"\" $proxy.UDPRelayMode) \"\") $proxy.ReduceRtt $proxy.DisableSNI (ne $sni \"\") $proxy.AllowInsecure }}&{{ $common }}{{- else }}?{{ $common }}{{- end }}#{{ $proxy.Name }}\n {{- else if eq $proxy.Type \"anytls\" }}\nanytls://{{ $password }}@{{ $server }}:{{ $proxy.Port }}{{- if ne $sni \"\" }}?sni={{ $sni }}&{{ $common }}{{- else }}?{{ $common }}{{- end }}#{{ $proxy.Name }}\n {{- end }}\n{{- end }}\n\n{{- range $proxy := .Proxies }}\n {{- if not (or (eq $proxy.Type \"shadowsocks\") (eq $proxy.Type \"vmess\") (eq $proxy.Type \"vless\") (eq $proxy.Type \"trojan\") (eq $proxy.Type \"hysteria2\") (eq $proxy.Type \"tuic\") (eq $proxy.Type \"anytls\")) }}\n# Skipped (unsupported protocol): {{ $proxy.Name }} ({{ $proxy.Type }})\n {{- end }}\n{{- end }}\n', 'base64', '{}', '2025-08-12 22:57:56.711', '2025-08-15 21:45:20.181'); +INSERT INTO `subscribe_application` (`id`, `name`, `icon`, `description`, `scheme`, `user_agent`, `is_default`, `subscribe_template`, `output_format`, `download_link`, `created_at`, `updated_at`) VALUES (2, 'Shadowrocket', '', '', 'shadowrocket://add/sub://${window.btoa(url)}?remark=${encodeURIComponent(name)}', 'Shadowrocket', 0, '{{- $GiB := 1073741824.0 -}}\n{{- $used := printf \"%.2f\" (divf (add (.UserInfo.Download | default 0 | float64) (.UserInfo.Upload | default 0 | float64)) $GiB) -}}\n{{- $traffic := (.UserInfo.Traffic | default 0 | float64) -}}\n{{- $total := printf \"%.2f\" (divf $traffic $GiB) -}}\n\n{{- $exp := \"\" -}}\n{{- $expStr := printf \"%v\" .UserInfo.ExpiredAt -}}\n{{- if regexMatch `^[0-9]+$` $expStr -}}\n {{- $ts := $expStr | float64 -}}\n {{- $sec := ternary (divf $ts 1000.0) $ts (ge (len $expStr) 13) -}}\n {{- $exp = (date \"2006-01-02 15:04:05\" (unixEpoch ($sec | int64))) -}}\n{{- else -}}\n {{- $exp = $expStr -}}\n{{- end -}}\n\nREMARKS={{ .SiteName }}-{{ .SubscribeName }}\nSTATUS=Traffic: {{ $used }} GiB/{{ $total }} GiB | Expires: {{ $exp }}\n\n{{- range $proxy := .Proxies }}\n {{- $server := $proxy.Server -}}\n {{- if and (contains $proxy.Server \":\") (not (hasPrefix \"[\" $proxy.Server)) -}}\n {{- $server = printf \"[%s]\" $proxy.Server -}}\n {{- end -}}\n\n {{- $sni := default \"\" $proxy.SNI -}}\n {{- if eq $sni \"\" -}}\n {{- $sni = default \"\" $proxy.Host -}}\n {{- end -}}\n {{- if and (eq $sni \"\") (not (or (regexMatch \"^[0-9]+\\\\.[0-9]+\\\\.[0-9]+\\\\.[0-9]+$\" $proxy.Server) (contains $proxy.Server \":\"))) -}}\n {{- $sni = $proxy.Server -}}\n {{- end -}}\n\n {{- $common := \"udp=1&tfo=1\" -}}\n\n {{- $password := $.UserInfo.Password -}}\n {{- if and (eq $proxy.Type \"shadowsocks\") (ne (default \"\" $proxy.ServerKey) \"\") -}}\n {{- $method := $proxy.Method -}}\n {{- if or (hasPrefix \"2022-blake3-\" $method) (eq $method \"2022-blake3-aes-128-gcm\") (eq $method \"2022-blake3-aes-256-gcm\") -}}\n {{- $userKeyLen := ternary 16 32 (hasSuffix \"128-gcm\" $method) -}}\n {{- $pwdStr := printf \"%s\" $password -}}\n {{- $userKey := ternary $pwdStr (trunc $userKeyLen $pwdStr) (le (len $pwdStr) $userKeyLen) -}}\n {{- $serverB64 := b64enc $proxy.ServerKey -}}\n {{- $userB64 := b64enc $userKey -}}\n {{- $password = printf \"%s:%s\" $serverB64 $userB64 -}}\n {{- end -}}\n {{- end -}}\n\n {{- if eq $proxy.Type \"shadowsocks\" }}\nss://{{ printf \"%s:%s\" (default \"aes-128-gcm\" $proxy.Method) $password | b64enc }}@{{ $server }}:{{ $proxy.Port }}?{{ $common }}#{{ $proxy.Name }}\n {{- else if eq $proxy.Type \"vmess\" }}\nvmess://{{ (dict \"v\" \"2\" \"ps\" $proxy.Name \"add\" $proxy.Server \"port\" (printf \"%d\" $proxy.Port) \"id\" $password \"aid\" \"0\" \"net\" (default \"tcp\" $proxy.Transport) \"type\" \"none\" \"host\" (default \"\" $proxy.Host) \"path\" (default \"\" $proxy.Path) \"tls\" (ternary \"tls\" \"\" (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\"))) \"sni\" $sni) | toJson | b64enc }}\n {{- else if eq $proxy.Type \"vless\" }}\nvless://{{ $password }}@{{ $server }}:{{ $proxy.Port }}?encryption=none{{- if ne (default \"\" $proxy.Flow) \"\" }}&flow={{ $proxy.Flow }}{{- end }}{{- if ne $proxy.Transport \"\" }}&type={{ $proxy.Transport }}{{- end }}{{- if and (eq $proxy.Transport \"ws\") (ne (default \"\" $proxy.Host) \"\") }}&host={{ $proxy.Host }}{{- end }}{{- if and (eq $proxy.Transport \"ws\") (ne (default \"\" $proxy.Path) \"\") }}&path={{ $proxy.Path | urlquery }}{{- end }}{{- if and (eq $proxy.Transport \"grpc\") (ne (default \"\" $proxy.ServiceName) \"\") }}&serviceName={{ $proxy.ServiceName }}{{- end }}{{- if or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\") }}&security={{ $proxy.Security }}{{- end }}{{- if ne $sni \"\" }}&sni={{ $sni }}{{- end }}{{- if $proxy.AllowInsecure }}&allowInsecure=1{{- end }}{{- if ne (default \"\" $proxy.Fingerprint) \"\" }}&fp={{ $proxy.Fingerprint }}{{- end }}{{- if and (eq $proxy.Security \"reality\") (ne (default \"\" $proxy.RealityPublicKey) \"\") }}&pbk={{ $proxy.RealityPublicKey }}{{- end }}{{- if and (eq $proxy.Security \"reality\") (ne (default \"\" $proxy.RealityShortId) \"\") }}&sid={{ $proxy.RealityShortId }}{{- end }}&{{ $common }}#{{ $proxy.Name }}\n {{- else if eq $proxy.Type \"trojan\" }}\ntrojan://{{ $password }}@{{ $server }}:{{ $proxy.Port }}{{- if or (and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) (ne $sni \"\")) (and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) $proxy.AllowInsecure) (ne $proxy.Transport \"\") }}?{{- end }}{{- if and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) (ne $sni \"\") }}sni={{ $sni }}{{- end }}{{- if and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) $proxy.AllowInsecure }}{{- if and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) (ne $sni \"\") }}&{{- end }}allowInsecure=1{{- end }}{{- if ne $proxy.Transport \"\" }}{{- if or (and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) (ne $sni \"\")) (and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) $proxy.AllowInsecure) }}&{{- end }}type={{ $proxy.Transport }}{{- end }}{{- if and (eq $proxy.Transport \"ws\") (ne (default \"\" $proxy.Host) \"\") }}{{- if or (and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) (ne $sni \"\")) (and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) $proxy.AllowInsecure) (ne $proxy.Transport \"\") }}&{{- end }}host={{ $proxy.Host }}{{- end }}{{- if and (eq $proxy.Transport \"ws\") (ne (default \"\" $proxy.Path) \"\") }}{{- if or (and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) (ne $sni \"\")) (and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) $proxy.AllowInsecure) (ne $proxy.Transport \"\") (and (eq $proxy.Transport \"ws\") (ne (default \"\" $proxy.Host) \"\")) }}&{{- end }}path={{ $proxy.Path | urlquery }}{{- end }}{{- if or (and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) (ne $sni \"\")) (and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) $proxy.AllowInsecure) (ne $proxy.Transport \"\") }}&{{ $common }}{{- else }}?{{ $common }}{{- end }}#{{ $proxy.Name }}\n {{- else if eq $proxy.Type \"hysteria2\" }}\nhysteria2://{{ $server }}:{{ $proxy.Port }}{{- if or (ne $password \"\") (ne $sni \"\") $proxy.AllowInsecure (ne (default \"\" $proxy.ObfsPassword) \"\") (ne (default \"\" $proxy.HopPorts) \"\") }}?{{- end }}{{- if ne $password \"\" }}auth={{ $password }}{{- end }}{{- if ne $sni \"\" }}{{- if ne $password \"\" }}&{{- end }}sni={{ $sni }}{{- end }}{{- if $proxy.AllowInsecure }}{{- if or (ne $password \"\") (ne $sni \"\") }}&{{- end }}insecure=1{{- end }}{{- if ne (default \"\" $proxy.ObfsPassword) \"\" }}{{- if or (ne $password \"\") (ne $sni \"\") $proxy.AllowInsecure }}&{{- end }}obfs=salamander&obfs-password={{ $proxy.ObfsPassword }}{{- end }}{{- if ne (default \"\" $proxy.HopPorts) \"\" }}{{- if or (ne $password \"\") (ne $sni \"\") $proxy.AllowInsecure (ne (default \"\" $proxy.ObfsPassword) \"\") }}&{{- end }}mport={{ $proxy.HopPorts }}{{- end }}{{- if or (ne $password \"\") (ne $sni \"\") $proxy.AllowInsecure (ne (default \"\" $proxy.ObfsPassword) \"\") (ne (default \"\" $proxy.HopPorts) \"\") }}&{{ $common }}{{- else }}?{{ $common }}{{- end }}#{{ $proxy.Name }}\n {{- else if eq $proxy.Type \"tuic\" }}\ntuic://{{ $password }}:{{ $password }}@{{ $server }}:{{ $proxy.Port }}{{- if or (ne (default \"\" $proxy.CongestionController) \"\") (ne (default \"\" $proxy.UDPRelayMode) \"\") $proxy.ReduceRtt $proxy.DisableSNI (ne $sni \"\") $proxy.AllowInsecure }}?{{- end }}{{- if ne (default \"\" $proxy.CongestionController) \"\" }}congestion_controller={{ $proxy.CongestionController }}{{- end }}{{- if ne (default \"\" $proxy.UDPRelayMode) \"\" }}{{- if ne (default \"\" $proxy.CongestionController) \"\" }}&{{- end }}udp_relay_mode={{ $proxy.UDPRelayMode }}{{- end }}{{- if $proxy.ReduceRtt }}{{- if or (ne (default \"\" $proxy.CongestionController) \"\") (ne (default \"\" $proxy.UDPRelayMode) \"\") }}&{{- end }}reduce_rtt=1{{- end }}{{- if $proxy.DisableSNI }}{{- if or (ne (default \"\" $proxy.CongestionController) \"\") (ne (default \"\" $proxy.UDPRelayMode) \"\") $proxy.ReduceRtt }}&{{- end }}disable_sni=1{{- end }}{{- if ne $sni \"\" }}{{- if or (ne (default \"\" $proxy.CongestionController) \"\") (ne (default \"\" $proxy.UDPRelayMode) \"\") $proxy.ReduceRtt $proxy.DisableSNI }}&{{- end }}sni={{ $sni }}{{- end }}{{- if $proxy.AllowInsecure }}{{- if or (ne (default \"\" $proxy.CongestionController) \"\") (ne (default \"\" $proxy.UDPRelayMode) \"\") $proxy.ReduceRtt $proxy.DisableSNI (ne $sni \"\") }}&{{- end }}allow_insecure=1{{- end }}{{- if or (ne (default \"\" $proxy.CongestionController) \"\") (ne (default \"\" $proxy.UDPRelayMode) \"\") $proxy.ReduceRtt $proxy.DisableSNI (ne $sni \"\") $proxy.AllowInsecure }}&{{ $common }}{{- else }}?{{ $common }}{{- end }}#{{ $proxy.Name }}\n {{- else if eq $proxy.Type \"anytls\" }}\nanytls://{{ $password }}@{{ $server }}:{{ $proxy.Port }}{{- if ne $sni \"\" }}?sni={{ $sni }}&{{ $common }}{{- else }}?{{ $common }}{{- end }}#{{ $proxy.Name }}\n {{- else }}\n# Unsupported protocol: {{ $proxy.Type }} - {{ $proxy.Name }}\n {{- end }}\n{{- end }}', 'base64', '{}', '2025-08-12 23:03:50.004', '2025-08-15 22:01:39.221'); +INSERT INTO `subscribe_application` (`id`, `name`, `icon`, `description`, `scheme`, `user_agent`, `is_default`, `subscribe_template`, `output_format`, `download_link`, `created_at`, `updated_at`) VALUES (3, 'Clash', '', '', 'clash://install-config?url=${url}&name=${name}', 'Clash', 0, '{{- $GiB := 1073741824.0 -}}\n{{- $used := printf \"%.2f\" (divf (add (.UserInfo.Download | default 0 | float64) (.UserInfo.Upload | default 0 | float64)) $GiB) -}}\n{{- $traffic := (.UserInfo.Traffic | default 0 | float64) -}}\n{{- $total := printf \"%.2f\" (divf $traffic $GiB) -}}\n\n{{- $exp := \"\" -}}\n{{- $expStr := printf \"%v\" .UserInfo.ExpiredAt -}}\n{{- if regexMatch `^[0-9]+$` $expStr -}}\n {{- $ts := $expStr | float64 -}}\n {{- $sec := ternary (divf $ts 1000.0) $ts (ge (len $expStr) 13) -}}\n {{- $exp = (date \"2006-01-02 15:04:05\" (unixEpoch ($sec | int64))) -}}\n{{- else -}}\n {{- $exp = $expStr -}}\n{{- end -}}\n\n{{- $supportedProxies := list -}}\n{{- range $proxy := .Proxies -}}\n {{- if or (eq $proxy.Type \"shadowsocks\") (eq $proxy.Type \"vmess\") (eq $proxy.Type \"vless\") (eq $proxy.Type \"trojan\") (eq $proxy.Type \"hysteria2\") (eq $proxy.Type \"tuic\") -}}\n {{- $supportedProxies = append $supportedProxies $proxy -}}\n {{- end -}}\n{{- end -}}\n\n{{- $proxyNames := \"\" -}}\n{{- range $proxy := $supportedProxies -}}\n {{- if eq $proxyNames \"\" -}}\n {{- $proxyNames = $proxy.Name -}}\n {{- else -}}\n {{- $proxyNames = printf \"%s, %s\" $proxyNames $proxy.Name -}}\n {{- end -}}\n{{- end -}}\n\n# {{ .SiteName }}-{{ .SubscribeName }}\n# Traffic: {{ $used }} GiB/{{ $total }} GiB | Expires: {{ $exp }}\n\nmode: rule\nipv6: true\nallow-lan: true\nbind-address: ''*''\nmixed-port: 6088\nlog-level: error\nunified-delay: true\ntcp-concurrent: true\nexternal-controller: ''0.0.0.0:9090''\ntun:\n enable: true\n stack: system\n auto-route: true\ndns:\n enable: true\n cache-algorithm: arc\n listen: ''0.0.0.0:1053''\n ipv6: true\n enhanced-mode: fake-ip\n fake-ip-range: 198.18.0.1/16\n fake-ip-filter: [''*.lan'', lens.l.google.com, ''*.srv.nintendo.net'', ''*.stun.playstation.net'', ''xbox.*.*.microsoft.com'', ''*.xboxlive.com'', ''*.msftncsi.com'', ''*.msftconnecttest.com'']\n default-nameserver: [119.29.29.29, 223.5.5.5]\n nameserver: [system, 119.29.29.29, 223.5.5.5]\n fallback: [8.8.8.8, 1.1.1.1]\n fallback-filter: { geoip: true, geoip-code: CN }\n\nproxies:\n{{- range $proxy := $supportedProxies }}\n {{- $server := $proxy.Server -}}\n {{- if and (contains $proxy.Server \":\") (not (hasPrefix \"[\" $proxy.Server)) -}}\n {{- $server = printf \"[%s]\" $proxy.Server -}}\n {{- end -}}\n\n {{- $sni := default \"\" $proxy.SNI -}}\n {{- if eq $sni \"\" -}}\n {{- $sni = default \"\" $proxy.Host -}}\n {{- end -}}\n {{- if and (eq $sni \"\") (not (or (regexMatch \"^[0-9]+\\\\.[0-9]+\\\\.[0-9]+\\\\.[0-9]+$\" $proxy.Server) (contains $proxy.Server \":\"))) -}}\n {{- $sni = $proxy.Server -}}\n {{- end -}}\n\n {{- $password := $.UserInfo.Password -}}\n {{- if and (eq $proxy.Type \"shadowsocks\") (ne (default \"\" $proxy.ServerKey) \"\") -}}\n {{- $method := $proxy.Method -}}\n {{- if or (hasPrefix \"2022-blake3-\" $method) (eq $method \"2022-blake3-aes-128-gcm\") (eq $method \"2022-blake3-aes-256-gcm\") -}}\n {{- $userKeyLen := ternary 16 32 (hasSuffix \"128-gcm\" $method) -}}\n {{- $pwdStr := printf \"%s\" $password -}}\n {{- $userKey := ternary $pwdStr (trunc $userKeyLen $pwdStr) (le (len $pwdStr) $userKeyLen) -}}\n {{- $serverB64 := b64enc $proxy.ServerKey -}}\n {{- $userB64 := b64enc $userKey -}}\n {{- $password = printf \"%s:%s\" $serverB64 $userB64 -}}\n {{- end -}}\n {{- end -}}\n\n {{- $common := \"udp: true, tfo: true\" -}}\n\n {{- if eq $proxy.Type \"shadowsocks\" }}\n - { name: {{ $proxy.Name | quote }}, type: ss, server: {{ $server }}, port: {{ $proxy.Port }}, cipher: {{ default \"aes-128-gcm\" $proxy.Method }}, password: {{ $password }}, {{ $common }}{{- if ne (default \"\" $proxy.Transport) \"\" }}, plugin: obfs, plugin-opts: { mode: http, host: {{ $sni }} }{{- end }} }\n {{- else if eq $proxy.Type \"vmess\" }}\n - { name: {{ $proxy.Name | quote }}, type: vmess, server: {{ $server }}, port: {{ $proxy.Port }}, uuid: {{ $password }}, alterId: 0, cipher: auto, {{ $common }}{{- if or (eq $proxy.Transport \"websocket\") (eq $proxy.Transport \"ws\") }}, network: ws, ws-opts: { path: {{ default \"/\" $proxy.Path }}{{- if ne (default \"\" $proxy.Host) \"\" }}, headers: { Host: {{ $proxy.Host }} }{{- end }} }{{- else if eq $proxy.Transport \"http\" }}, network: http, http-opts: { method: GET, path: [{{ default \"/\" $proxy.Path | quote }}]{{- if ne (default \"\" $proxy.Host) \"\" }}, headers: { Host: [{{ $proxy.Host | quote }}] }{{- end }} }{{- else if eq $proxy.Transport \"grpc\" }}, network: grpc, grpc-opts: { grpc-service-name: {{ default \"grpc\" $proxy.ServiceName }} }{{- end }}{{- if or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\") }}, tls: true{{- end }}{{- if ne $sni \"\" }}, servername: {{ $sni }}{{- end }}{{- if $proxy.AllowInsecure }}, skip-cert-verify: true{{- end }}{{- if ne (default \"\" $proxy.Fingerprint) \"\" }}, fingerprint: {{ $proxy.Fingerprint }}{{- end }} }\n {{- else if eq $proxy.Type \"vless\" }}\n - { name: {{ $proxy.Name | quote }}, type: vless, server: {{ $server }}, port: {{ $proxy.Port }}, uuid: {{ $password }}, {{ $common }}{{- if or (eq $proxy.Transport \"ws\") (eq $proxy.Transport \"websocket\") }}, network: ws, ws-opts: { path: {{ default \"/\" $proxy.Path }}{{- if ne (default \"\" $proxy.Host) \"\" }}, headers: { Host: {{ $proxy.Host }} }{{- end }} }{{- else if eq $proxy.Transport \"http\" }}, network: http, http-opts: { method: GET, path: [{{ default \"/\" $proxy.Path | quote }}]{{- if ne (default \"\" $proxy.Host) \"\" }}, headers: { Host: [{{ $proxy.Host | quote }}] }{{- end }} }{{- else if eq $proxy.Transport \"grpc\" }}, network: grpc, grpc-opts: { grpc-service-name: {{ default \"grpc\" $proxy.ServiceName }} }{{- end }}{{- if ne $sni \"\" }}, servername: {{ $sni }}{{- end }}{{- if $proxy.AllowInsecure }}, skip-cert-verify: true{{- end }}{{- if ne (default \"\" $proxy.Fingerprint) \"\" }}, fingerprint: {{ $proxy.Fingerprint }}{{- end }}{{- if and (eq $proxy.Security \"reality\") (ne (default \"\" $proxy.RealityPublicKey) \"\") }}, reality-opts: { public-key: {{ $proxy.RealityPublicKey }}{{- if ne (default \"\" $proxy.RealityShortId) \"\" }}, short-id: {{ $proxy.RealityShortId }}{{- end }} }{{- end }}{{- if ne (default \"\" $proxy.Flow) \"\" }}, flow: {{ $proxy.Flow }}{{- end }} }\n {{- else if eq $proxy.Type \"trojan\" }}\n - { name: {{ $proxy.Name | quote }}, type: trojan, server: {{ $server }}, port: {{ $proxy.Port }}, password: {{ $password }}, {{ $common }}{{- if ne $sni \"\" }}, sni: {{ $sni }}{{- end }}{{- if $proxy.AllowInsecure }}, skip-cert-verify: true{{- end }}{{- if ne (default \"\" $proxy.Fingerprint) \"\" }}, fingerprint: {{ $proxy.Fingerprint }}{{- end }}{{- if or (eq $proxy.Transport \"ws\") (eq $proxy.Transport \"websocket\") }}, network: ws, ws-opts: { path: {{ default \"/\" $proxy.Path }}{{- if ne (default \"\" $proxy.Host) \"\" }}, headers: { Host: {{ $proxy.Host }} }{{- end }} }{{- else if eq $proxy.Transport \"http\" }}, network: http, http-opts: { method: GET, path: [{{ default \"/\" $proxy.Path | quote }}]{{- if ne (default \"\" $proxy.Host) \"\" }}, headers: { Host: [{{ $proxy.Host | quote }}] }{{- end }} }{{- else if eq $proxy.Transport \"grpc\" }}, network: grpc, grpc-opts: { grpc-service-name: {{ default \"grpc\" $proxy.ServiceName }} }{{- end }} }\n {{- else if or (eq $proxy.Type \"hysteria2\") (eq $proxy.Type \"hy2\") }}\n - { name: {{ $proxy.Name | quote }}, type: hysteria2, server: {{ $server }}, port: {{ $proxy.Port }}, password: {{ $password }}, {{ $common }}{{- if ne $sni \"\" }}, sni: {{ $sni }}{{- end }}{{- if $proxy.AllowInsecure }}, skip-cert-verify: true{{- end }}{{- if ne (default \"\" $proxy.ObfsPassword) \"\" }}, obfs: salamander, obfs-password: {{ $proxy.ObfsPassword }}{{- end }}{{- if ne (default \"\" $proxy.HopPorts) \"\" }}, ports: {{ $proxy.HopPorts }}{{- end }}{{- if ne (default 0 $proxy.HopInterval) 0 }}, hop-interval: {{ $proxy.HopInterval }}{{- end }} }\n {{- else if eq $proxy.Type \"tuic\" }}\n - { name: {{ $proxy.Name | quote }}, type: tuic, server: {{ $server }}, port: {{ $proxy.Port }}, uuid: {{ default \"\" $proxy.ServerKey }}, password: {{ $password }}, {{ $common }}{{- if ne $sni \"\" }}, sni: {{ $sni }}{{- end }}{{- if $proxy.AllowInsecure }}, skip-cert-verify: true{{- end }}{{- if $proxy.DisableSNI }}, disable-sni: true{{- end }}{{- if $proxy.ReduceRtt }}, reduce-rtt: true{{- end }}{{- if ne (default \"\" $proxy.UDPRelayMode) \"\" }}, udp-relay-mode: {{ $proxy.UDPRelayMode }}{{- end }}{{- if ne (default \"\" $proxy.CongestionController) \"\" }}, congestion-controller: {{ $proxy.CongestionController }}{{- end }} }\n {{- else if eq $proxy.Type \"wireguard\" }}\n - { name: {{ $proxy.Name | quote }}, type: wireguard, server: {{ $server }}, port: {{ $proxy.Port }}, private-key: {{ default \"\" $proxy.ServerKey }}, public-key: {{ default \"\" $proxy.RealityPublicKey }}, {{ $common }}{{- if ne (default \"\" $proxy.Path) \"\" }}, preshared-key: {{ $proxy.Path }}{{- end }}{{- if ne (default \"\" $proxy.RealityServerAddr) \"\" }}, ip: {{ $proxy.RealityServerAddr }}{{- end }}{{- if ne (default 0 $proxy.RealityServerPort) 0 }}, ipv6: {{ $proxy.RealityServerPort }}{{- end }} }\n {{- else if eq $proxy.Type \"anytls\" }}\n - { name: {{ $proxy.Name | quote }}, type: anytls, server: {{ $server }}, port: {{ $proxy.Port }}, password: {{ $password }}, {{ $common }} }\n {{- else }}\n - { name: {{ $proxy.Name | quote }}, type: {{ $proxy.Type }}, server: {{ $server }}, port: {{ $proxy.Port }}, {{ $common }} }\n {{- end }}\n{{- end }}\n\n{{- range $proxy := .Proxies }}\n {{- if not (or (eq $proxy.Type \"shadowsocks\") (eq $proxy.Type \"vmess\") (eq $proxy.Type \"vless\") (eq $proxy.Type \"trojan\") (eq $proxy.Type \"hysteria2\") (eq $proxy.Type \"tuic\")) }}\n# Skipped (unsupported by Clash): {{ $proxy.Name }} ({{ $proxy.Type }})\n {{- end }}\n{{- end }}\n\nproxy-groups:\n - { name: 🚀 Proxy, type: select, proxies: [🌏 Auto, 🎯 Direct, {{ $proxyNames }}] }\n - { name: 🍎 Apple, type: select, proxies: [🚀 Proxy, 🎯 Direct, {{ $proxyNames }}] }\n - { name: 🔍 Google, type: select, proxies: [🚀 Proxy, 🎯 Direct, {{ $proxyNames }}] }\n - { name: 🪟 Microsoft, type: select, proxies: [🚀 Proxy, 🎯 Direct, {{ $proxyNames }}] }\n - { name: 📺 GlobalMedia, type: select, proxies: [🚀 Proxy, 🎯 Direct, {{ $proxyNames }}] }\n - { name: 📟 Telegram, type: select, proxies: [🚀 Proxy, 🎯 Direct, {{ $proxyNames }}] }\n - { name: 🤖 AI, type: select, proxies: [🚀 Proxy, 🎯 Direct, {{ $proxyNames }}] }\n - { name: 🪙 Crypto, type: select, proxies: [🚀 Proxy, 🎯 Direct, {{ $proxyNames }}] }\n - { name: 🎮 Game, type: select, proxies: [🚀 Proxy, 🎯 Direct, {{ $proxyNames }}] }\n - { name: 🇨🇳 China, type: select, proxies: [🎯 Direct, 🚀 Proxy, {{ $proxyNames }}] }\n - { name: 🎯 Direct, type: select, proxies: [DIRECT], hidden: true }\n - { name: 🐠 Final, type: select, proxies: [🚀 Proxy, 🎯 Direct, {{ $proxyNames }}] }\n - { name: 🌏 Auto, type: url-test, proxies: [{{ $proxyNames }}] }\n\nrules:\n - RULE-SET, Apple, 🍎 Apple\n - RULE-SET, Google, 🔍 Google\n - RULE-SET, Microsoft, 🪟 Microsoft\n - RULE-SET, Github, 🪟 Microsoft\n - RULE-SET, HBO, 📺 GlobalMedia\n - RULE-SET, Disney, 📺 GlobalMedia\n - RULE-SET, TikTok, 📺 GlobalMedia\n - RULE-SET, Netflix, 📺 GlobalMedia\n - RULE-SET, GlobalMedia, 📺 GlobalMedia\n - RULE-SET, Telegram, 📟 Telegram\n - RULE-SET, OpenAI, 🤖 AI\n - RULE-SET, Gemini, 🤖 AI\n - RULE-SET, Copilot, 🤖 AI\n - RULE-SET, Claude, 🤖 AI\n - RULE-SET, Crypto, 🪙 Crypto\n - RULE-SET, Cryptocurrency, 🪙 Crypto\n - RULE-SET, Game, 🎮 Game\n - RULE-SET, Global, 🚀 Proxy\n - RULE-SET, ChinaMax, 🇨🇳 China\n - RULE-SET, Lan, 🎯 Direct\n - GEOIP, CN, 🇨🇳 China\n - MATCH, 🐠 Final\n\nrule-providers:\n Apple:\n type: http\n behavior: classical\n format: yaml\n url: https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Clash/Apple/Apple_Classical_No_Resolve.yaml\n interval: 86400\n Google:\n type: http\n behavior: classical\n format: yaml\n url: https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Clash/Google/Google_No_Resolve.yaml\n interval: 86400\n Microsoft:\n type: http\n behavior: classical\n format: yaml\n url: https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Clash/Microsoft/Microsoft.yaml\n interval: 86400\n Github:\n type: http\n behavior: classical\n format: yaml\n url: https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Clash/GitHub/GitHub.yaml\n interval: 86400\n HBO:\n type: http\n behavior: classical\n format: yaml\n url: https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Clash/HBO/HBO.yaml\n interval: 86400\n Disney:\n type: http\n behavior: classical\n format: yaml\n url: https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Clash/Disney/Disney.yaml\n interval: 86400\n TikTok:\n type: http\n behavior: classical\n format: yaml\n url: https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Clash/TikTok/TikTok.yaml\n interval: 86400\n Netflix:\n type: http\n behavior: classical\n format: yaml\n url: https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Clash/Netflix/Netflix.yaml\n interval: 86400\n GlobalMedia:\n type: http\n behavior: classical\n format: yaml\n url: https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Clash/GlobalMedia/GlobalMedia_Classical_No_Resolve.yaml\n interval: 86400\n Telegram:\n type: http\n behavior: classical\n format: yaml\n url: https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Clash/Telegram/Telegram_No_Resolve.yaml\n interval: 86400\n OpenAI:\n type: http\n behavior: classical\n format: yaml\n url: https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Clash/OpenAI/OpenAI.yaml\n interval: 86400\n Gemini:\n type: http\n behavior: classical\n format: yaml\n url: https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Clash/Gemini/Gemini.yaml\n interval: 86400\n Copilot:\n type: http\n behavior: classical\n format: yaml\n url: https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Clash/Copilot/Copilot.yaml\n interval: 86400\n Claude:\n type: http\n behavior: classical\n format: yaml\n url: https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Clash/Claude/Claude.yaml\n interval: 86400\n Crypto:\n type: http\n behavior: classical\n format: yaml\n url: https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Clash/Crypto/Crypto.yaml\n interval: 86400\n Cryptocurrency:\n type: http\n behavior: classical\n format: yaml\n url: https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Clash/Cryptocurrency/Cryptocurrency.yaml\n interval: 86400\n Game:\n type: http\n behavior: classical\n format: yaml\n url: https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Clash/Game/Game.yaml\n interval: 86400\n Global:\n type: http\n behavior: classical\n format: yaml\n url: https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Clash/Global/Global_Classical_No_Resolve.yaml\n interval: 86400\n ChinaMax:\n type: http\n behavior: classical\n format: yaml\n url: https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Clash/ChinaMax/ChinaMax_Classical_No_Resolve.yaml\n interval: 86400\n Lan:\n type: http\n behavior: classical\n format: yaml\n url: https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Clash/Lan/Lan.yaml\n interval: 86400\n\nurl-rewrite:\n - ^https?:\\/\\/(www.)?g\\.cn https://www.google.com 302\n - ^https?:\\/\\/(www.)?google\\.cn https://www.google.com 302\n', 'yaml', '{}', '2025-08-12 23:10:00.487', '2025-08-15 22:01:27.031'); +INSERT INTO `subscribe_application` (`id`, `name`, `icon`, `description`, `scheme`, `user_agent`, `is_default`, `subscribe_template`, `output_format`, `download_link`, `created_at`, `updated_at`) VALUES (4, 'SingBox', '', '', 'sing-box://import-remote-profile?url=${encodeURIComponent(url)}#${name}', 'sing-box', 0, '{{- $GiB := 1073741824.0 -}}\n{{- $used := printf \"%.2f\" (divf (add (.UserInfo.Download | default 0 | float64) (.UserInfo.Upload | default 0 | float64)) $GiB) -}}\n{{- $traffic := (.UserInfo.Traffic | default 0 | float64) -}}\n{{- $total := printf \"%.2f\" (divf $traffic $GiB) -}}\n\n{{- $exp := \"\" -}}\n{{- $expStr := printf \"%v\" .UserInfo.ExpiredAt -}}\n{{- if regexMatch `^[0-9]+$` $expStr -}}\n {{- $ts := $expStr | float64 -}}\n {{- $sec := ternary (divf $ts 1000.0) $ts (ge (len $expStr) 13) -}}\n {{- $exp = (date \"2006-01-02 15:04:05\" (unixEpoch ($sec | int64))) -}}\n{{- else -}}\n {{- $exp = $expStr -}}\n{{- end -}}\n\n{{- $supportedProxies := list -}}\n{{- range $proxy := .Proxies -}}\n {{- if or (eq $proxy.Type \"shadowsocks\") (eq $proxy.Type \"vmess\") (eq $proxy.Type \"vless\") (eq $proxy.Type \"trojan\") (eq $proxy.Type \"hysteria2\") (eq $proxy.Type \"hy2\") (eq $proxy.Type \"tuic\") -}}\n {{- $supportedProxies = append $supportedProxies $proxy -}}\n {{- end -}}\n{{- end -}}\n\n{{- $proxyNames := \"\" -}}\n{{- if gt (len $supportedProxies) 0 -}}\n {{- range $proxy := $supportedProxies -}}\n {{- if eq $proxyNames \"\" -}}\n {{- $proxyNames = printf \"\\\"%s\\\"\" $proxy.Name -}}\n {{- else -}}\n {{- $proxyNames = printf \"%s, \\\"%s\\\"\" $proxyNames $proxy.Name -}}\n {{- end -}}\n {{- end -}}\n {{- $proxyNames = printf \", %s\" $proxyNames -}}\n{{- end -}}\n\n\n{\n \"log\": {\"level\": \"info\", \"timestamp\": true},\n \"experimental\": {\n \"cache_file\": {\"enabled\": true, \"path\": \"cache.db\", \"cache_id\": \"my_profile\", \"store_fakeip\": false},\n \"clash_api\": {\"external_controller\": \"127.0.0.1:9090\", \"external_ui\": \"ui\", \"secret\": \"\", \"external_ui_download_url\": \"https://mirror.ghproxy.com/https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip\", \"external_ui_download_detour\": \"direct\", \"default_mode\": \"rule\"}\n },\n \"dns\": {\n \"servers\": [\n {\"tag\": \"dns_proxy\",\"address\": \"tls://8.8.8.8\",\"detour\": \"Proxy\"},\n {\"tag\": \"dns_direct\",\"address\": \"https://223.5.5.5/dns-query\",\"detour\": \"direct\"}\n ],\n \"rules\": [\n {\"rule_set\": \"geosite-cn\", \"server\": \"dns_direct\"},\n {\"clash_mode\": \"direct\", \"server\": \"dns_direct\"},\n {\"clash_mode\": \"global\", \"server\": \"dns_proxy\"},\n {\"rule_set\": \"geosite-geolocation-!cn\", \"server\": \"dns_proxy\"}\n ],\n \"final\": \"dns_direct\",\n \"strategy\": \"ipv4_only\"\n },\n \"inbounds\": [\n {\"tag\": \"tun-in\", \"type\": \"tun\", \"address\": [\"172.18.0.1/30\",\"fdfe:dcba:9876::1/126\"], \"auto_route\": true, \"strict_route\": true, \"stack\": \"system\",\n \"platform\": {\"http_proxy\": {\"enabled\": true, \"server\": \"127.0.0.1\", \"server_port\": 7890}}},\n {\"tag\": \"mixed-in\", \"type\": \"mixed\", \"listen\": \"127.0.0.1\", \"listen_port\": 7890}\n ],\n \"outbounds\": [\n {\"tag\": \"Proxy\", \"type\": \"selector\", \"outbounds\": [\"Auto - UrlTest\", \"direct\"{{ $proxyNames }}]},\n {\"tag\": \"Domestic\", \"type\": \"selector\", \"outbounds\": [\"direct\", \"Proxy\"{{ $proxyNames }}]},\n {\"tag\": \"Others\", \"type\": \"selector\", \"outbounds\": [\"Proxy\", \"direct\"{{ $proxyNames }}]},\n {\"tag\": \"AI Suite\", \"type\": \"selector\", \"outbounds\": [\"Proxy\", \"direct\"{{ $proxyNames }}]},\n {\"tag\": \"Netflix\", \"type\": \"selector\", \"outbounds\": [\"Proxy\", \"direct\"{{ $proxyNames }}]},\n {\"tag\": \"Disney Plus\", \"type\": \"selector\", \"outbounds\": [\"Proxy\", \"direct\"{{ $proxyNames }}]},\n {\"tag\": \"YouTube\", \"type\": \"selector\", \"outbounds\": [\"Proxy\", \"direct\"{{ $proxyNames }}]},\n {\"tag\": \"Max\", \"type\": \"selector\", \"outbounds\": [\"Proxy\", \"direct\"{{ $proxyNames }}]},\n {\"tag\": \"Spotify\", \"type\": \"selector\", \"outbounds\": [\"Proxy\", \"direct\"{{ $proxyNames }}]},\n {\"tag\": \"Apple\", \"type\": \"selector\", \"outbounds\": [\"direct\", \"Proxy\"{{ $proxyNames }}]},\n {\"tag\": \"Telegram\", \"type\": \"selector\", \"outbounds\": [\"Proxy\", \"direct\"{{ $proxyNames }}]},\n {\"tag\": \"Microsoft\", \"type\": \"selector\", \"outbounds\": [\"Proxy\", \"direct\"{{ $proxyNames }}]},\n {\"tag\": \"Tiktok\", \"type\": \"selector\", \"outbounds\": [\"Proxy\", \"direct\"{{ $proxyNames }}]},\n {\"tag\": \"AdBlock\", \"type\": \"selector\", \"outbounds\": [\"block\", \"direct\", \"Proxy\"]},\n {{- if gt (len $supportedProxies) 0 }}\n {\"tag\": \"Auto - UrlTest\", \"type\": \"urltest\", \"outbounds\": [{{ $proxyNames | trimPrefix \", \" }}], \"url\": \"http://cp.cloudflare.com/\", \"interval\": \"10m\", \"tolerance\": 50}\n {{- range $i, $proxy := $supportedProxies }},\n{{- $server := $proxy.Server -}}\n{{- if and (contains $proxy.Server \":\") (not (hasPrefix \"[\" $proxy.Server)) -}}\n {{- $server = printf \"[%s]\" $proxy.Server -}}\n{{- end -}}\n\n{{- $sni := default \"\" $proxy.SNI -}}\n{{- if eq $sni \"\" -}}\n {{- $sni = default \"\" $proxy.Host -}}\n{{- end -}}\n{{- if and (eq $sni \"\") (not (or (regexMatch \"^[0-9]+\\\\.[0-9]+\\\\.[0-9]+\\\\.[0-9]+$\" $proxy.Server) (contains $proxy.Server \":\"))) -}}\n {{- $sni = $proxy.Server -}}\n{{- end -}}\n\n{{- $password := $.UserInfo.Password -}}\n{{- if and (eq $proxy.Type \"shadowsocks\") (ne (default \"\" $proxy.ServerKey) \"\") -}}\n {{- $method := $proxy.Method -}}\n {{- if or (hasPrefix \"2022-blake3-\" $method) (eq $method \"2022-blake3-aes-128-gcm\") (eq $method \"2022-blake3-aes-256-gcm\") -}}\n {{- $userKeyLen := ternary 16 32 (hasSuffix \"128-gcm\" $method) -}}\n {{- $pwdStr := printf \"%s\" $password -}}\n {{- $userKey := ternary $pwdStr (trunc $userKeyLen $pwdStr) (le (len $pwdStr) $userKeyLen) -}}\n {{- $serverB64 := b64enc $proxy.ServerKey -}}\n {{- $userB64 := b64enc $userKey -}}\n {{- $password = printf \"%s:%s\" $serverB64 $userB64 -}}\n {{- end -}}\n{{- end -}}\n\n{{- $common := `\"tcp_fast_open\": true, \"udp_over_tcp\": false` -}}\n\n{{- if eq $proxy.Type \"shadowsocks\" -}}\n {{- $method := default \"aes-128-gcm\" $proxy.Method -}}\n { \"type\": \"shadowsocks\", \"tag\": {{ $proxy.Name | quote }}, \"server\": {{ $server | quote }}, \"server_port\": {{ $proxy.Port }}, \"method\": {{ $method | quote }}, \"password\": {{ $password | quote }}, {{ $common }} }\n{{- else if eq $proxy.Type \"trojan\" -}}\n { \"type\": \"trojan\", \"tag\": {{ $proxy.Name | quote }}, \"server\": {{ $server | quote }}, \"server_port\": {{ $proxy.Port }}, \"password\": {{ $password | quote }}{{- if or (eq $proxy.Transport \"ws\") (eq $proxy.Transport \"websocket\") }}, \"transport\": {\"type\": \"ws\", \"path\": {{ default \"/\" $proxy.Path | quote }}{{- if ne (default \"\" $proxy.Host) \"\" }}, \"headers\": {\"Host\": {{ $proxy.Host | quote }} }{{- end -}}}{{- else if eq $proxy.Transport \"grpc\" }}, \"transport\": {\"type\": \"grpc\", \"service_name\": {{ default \"grpc\" $proxy.ServiceName | quote }}}{{- end }}, {{ $common }}, \"tls\": {\"enabled\": true{{- if ne $sni \"\" }}, \"server_name\": {{ $sni | quote }}{{- end }}{{- if $proxy.AllowInsecure }}, \"insecure\": true{{- end }}{{- if ne (default \"\" $proxy.Fingerprint) \"\" }}, \"utls\": {\"enabled\": true, \"fingerprint\": {{ $proxy.Fingerprint | quote }} }{{- end }}} }\n{{- else if eq $proxy.Type \"vless\" -}}\n { \"type\": \"vless\", \"tag\": {{ $proxy.Name | quote }}, \"server\": {{ $server | quote }}, \"server_port\": {{ $proxy.Port }}, \"uuid\": {{ $password | quote }}{{- if ne (default \"\" $proxy.Flow) \"\" }}, \"flow\": {{ $proxy.Flow | quote }}{{- end }}{{- if or (eq $proxy.Transport \"ws\") (eq $proxy.Transport \"websocket\") }}, \"transport\": {\"type\": \"ws\", \"path\": {{ default \"/\" $proxy.Path | quote }}{{- if ne (default \"\" $proxy.Host) \"\" }}, \"headers\": {\"Host\": {{ $proxy.Host | quote }} }{{- end -}}}{{- else if eq $proxy.Transport \"grpc\" }}, \"transport\": {\"type\": \"grpc\", \"service_name\": {{ default \"grpc\" $proxy.ServiceName | quote }}}{{- end }}, {{ $common }}{{- if ne (default \"\" $proxy.RealityPublicKey) \"\" }}, \"reality\": { \"enabled\": true, \"public_key\": {{ $proxy.RealityPublicKey | quote }}{{- if ne (default \"\" $proxy.RealityShortId) \"\" }}, \"short_id\": {{ $proxy.RealityShortId | quote }}{{- end }}{{- if ne $sni \"\" }}, \"server_name\": {{ $sni | quote }}{{- end }} }{{- else if or (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) (ne $sni \"\") $proxy.AllowInsecure (ne (default \"\" $proxy.Fingerprint) \"\") }}, \"tls\": {\"enabled\": true{{- if ne $sni \"\" }}, \"server_name\": {{ $sni | quote }}{{- end }}{{- if $proxy.AllowInsecure }}, \"insecure\": true{{- end }}{{- if ne (default \"\" $proxy.Fingerprint) \"\" }}, \"utls\": {\"enabled\": true, \"fingerprint\": {{ $proxy.Fingerprint | quote }} }{{- end }}}{{- end }} }\n{{- else if eq $proxy.Type \"vmess\" -}}\n { \"type\": \"vmess\", \"tag\": {{ $proxy.Name | quote }}, \"server\": {{ $server | quote }}, \"server_port\": {{ $proxy.Port }}, \"uuid\": {{ $password | quote }}, \"security\": \"auto\", {{ $common }}{{- if or (eq $proxy.Transport \"ws\") (eq $proxy.Transport \"websocket\") }}, \"transport\": {\"type\": \"ws\", \"path\": {{ default \"/\" $proxy.Path | quote }}{{- if ne (default \"\" $proxy.Host) \"\" }}, \"headers\": {\"Host\": {{ $proxy.Host | quote }} }{{- end -}}}{{- else if eq $proxy.Transport \"grpc\" }}, \"transport\": {\"type\": \"grpc\", \"service_name\": {{ default \"grpc\" $proxy.ServiceName | quote }}}{{- end }}{{- if or (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) (ne $sni \"\") $proxy.AllowInsecure (ne (default \"\" $proxy.Fingerprint) \"\") }}, \"tls\": {\"enabled\": true{{- if ne $sni \"\" }}, \"server_name\": {{ $sni | quote }}{{- end }}{{- if $proxy.AllowInsecure }}, \"insecure\": true{{- end }}{{- if ne (default \"\" $proxy.Fingerprint) \"\" }}, \"utls\": {\"enabled\": true, \"fingerprint\": {{ $proxy.Fingerprint | quote }} }{{- end }}}{{- end }} }\n{{- else if or (eq $proxy.Type \"hysteria2\") (eq $proxy.Type \"hy2\") -}}\n { \"type\": \"hysteria2\", \"tag\": {{ $proxy.Name | quote }}, \"server\": {{ $server | quote }}, \"server_port\": {{ $proxy.Port }}, \"password\": {{ $password | quote }}{{- if ne (default \"\" $proxy.ObfsPassword) \"\" }}, \"obfs\": { \"type\": \"salamander\", \"password\": {{ $proxy.ObfsPassword | quote }} }{{- end }}{{- if ne (default \"\" $proxy.HopPorts) \"\" }}, \"ports\": {{ $proxy.HopPorts | quote }}{{- end }}{{- if ne (default 0 $proxy.HopInterval) 0 }}, \"hop_interval\": {{ $proxy.HopInterval }}{{- end }}, {{ $common }}, \"tls\": {\"enabled\": true{{- if ne $sni \"\" }}, \"server_name\": {{ $sni | quote }}{{- end }}{{- if $proxy.AllowInsecure }}, \"insecure\": true{{- end }}{{- if ne (default \"\" $proxy.Fingerprint) \"\" }}, \"utls\": {\"enabled\": true, \"fingerprint\": {{ $proxy.Fingerprint | quote }} }{{- end }}} }\n{{- else if eq $proxy.Type \"tuic\" -}}\n { \"type\": \"tuic\", \"tag\": {{ $proxy.Name | quote }}, \"server\": {{ $server | quote }}, \"server_port\": {{ $proxy.Port }}, \"uuid\": {{ default \"\" $proxy.ServerKey | quote }}, \"password\": {{ $password | quote }}{{- if $proxy.DisableSNI }}, \"disable_sni\": true{{- end }}{{- if $proxy.ReduceRtt }}, \"reduce_rtt\": true{{- end }}{{- if ne (default \"\" $proxy.UDPRelayMode) \"\" }}, \"udp_relay_mode\": {{ $proxy.UDPRelayMode | quote }}{{- end }}{{- if ne (default \"\" $proxy.CongestionController) \"\" }}, \"congestion_control\": {{ $proxy.CongestionController | quote }}{{- end }}, {{ $common }}, \"alpn\": [\"h3\"], \"tls\": {\"enabled\": true{{- if ne $sni \"\" }}, \"server_name\": {{ $sni | quote }}{{- end }}{{- if $proxy.AllowInsecure }}, \"insecure\": true{{- end }}{{- if ne (default \"\" $proxy.Fingerprint) \"\" }}, \"utls\": {\"enabled\": true, \"fingerprint\": {{ $proxy.Fingerprint | quote }} }{{- end }}} }\n{{- else if eq $proxy.Type \"anytls\" -}}\n { \"type\": \"anytls\", \"tag\": {{ $proxy.Name | quote }}, \"server\": {{ $server | quote }}, \"server_port\": {{ $proxy.Port }}, \"password\": {{ $password | quote }}, {{ $common }}, \"tls\": {\"enabled\": true{{- if ne $sni \"\" }}, \"server_name\": {{ $sni | quote }}{{- end }}{{- if $proxy.AllowInsecure }}, \"insecure\": true{{- end }}{{- if ne (default \"\" $proxy.Fingerprint) \"\" }}, \"utls\": {\"enabled\": true, \"fingerprint\": {{ $proxy.Fingerprint | quote }} }{{- end }}} }\n{{- else if eq $proxy.Type \"wireguard\" -}}\n { \"type\": \"wireguard\", \"tag\": {{ $proxy.Name | quote }}, \"server\": {{ $server | quote }}, \"server_port\": {{ $proxy.Port }}, \"private_key\": {{ default \"\" $proxy.ServerKey | quote }}, \"peer_public_key\": {{ default \"\" $proxy.RealityPublicKey | quote }}{{- if ne (default \"\" $proxy.Path) \"\" }}, \"pre_shared_key\": {{ $proxy.Path | quote }}{{- end }}{{- if ne (default \"\" $proxy.RealityServerAddr) \"\" }}, \"local_address\": [{{ $proxy.RealityServerAddr | quote }}]{{- end }}, {{ $common }} }\n{{- else -}}\n { \"type\": \"direct\", \"tag\": {{ $proxy.Name | quote }}, {{ $common }} }\n{{- end }}\n {{- end }},\n {{- end }}\n {\"type\": \"direct\", \"tag\": \"direct\"},\n {\"type\": \"block\", \"tag\": \"block\"}\n ],\n \"route\": {\n \"auto_detect_interface\": true, \"final\": \"Proxy\",\n \"rules\": [\n {\"type\": \"logical\", \"mode\": \"or\", \"rules\": [{\"port\": 53},{\"protocol\": \"dns\"}], \"action\": \"hijack-dns\"},\n {\"rule_set\": \"geosite-category-ads-all\", \"outbound\": \"AdBlock\"},\n {\"clash_mode\": \"direct\", \"outbound\": \"direct\"},\n {\"clash_mode\": \"global\", \"outbound\": \"Proxy\"},\n {\"domain\": [\"clash.razord.top\",\"yacd.metacubex.one\",\"yacd.haishan.me\",\"d.metacubex.one\"], \"outbound\": \"direct\"},\n {\"ip_is_private\": true, \"outbound\": \"direct\"},\n {\"rule_set\": [\"geoip-netflix\",\"geosite-netflix\"], \"outbound\": \"Netflix\"},\n {\"rule_set\": \"geosite-disney\", \"outbound\": \"Disney Plus\"},\n {\"rule_set\": \"geosite-youtube\", \"outbound\": \"YouTube\"},\n {\"rule_set\": \"geosite-max\", \"outbound\": \"Max\"},\n {\"rule_set\": \"geosite-spotify\", \"outbound\": \"Spotify\"},\n {\"rule_set\": [\"geoip-apple\",\"geosite-apple\"], \"outbound\": \"Apple\"},\n {\"rule_set\": [\"geoip-telegram\",\"geosite-telegram\"], \"outbound\": \"Telegram\"},\n {\"rule_set\": \"geosite-openai\", \"outbound\": \"AI Suite\"},\n {\"rule_set\": \"geosite-microsoft\", \"outbound\": \"Microsoft\"},\n {\"rule_set\": \"geosite-tiktok\", \"outbound\": \"Tiktok\"},\n {\"rule_set\": \"geosite-private\", \"outbound\": \"direct\"},\n {\"rule_set\": [\"geoip-cn\",\"geosite-cn\"], \"outbound\": \"Domestic\"},\n {\"rule_set\": \"geosite-geolocation-!cn\", \"outbound\": \"Others\"}\n ],\n \"rule_set\": [\n {\"tag\": \"geoip-cn\",\"type\": \"remote\",\"format\": \"binary\",\"url\": \"https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geoip/cn.srs\",\"download_detour\": \"direct\"},\n {\"tag\": \"geosite-cn\",\"type\": \"remote\",\"format\": \"binary\",\"url\": \"https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/cn.srs\",\"download_detour\": \"direct\"},\n {\"tag\": \"geosite-private\",\"type\": \"remote\",\"format\": \"binary\",\"url\": \"https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/private.srs\",\"download_detour\": \"direct\"},\n {\"tag\": \"geosite-geolocation-!cn\",\"type\": \"remote\",\"format\": \"binary\",\"url\": \"https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/geolocation-!cn.srs\",\"download_detour\": \"direct\"},\n {\"tag\": \"geosite-category-ads-all\",\"type\": \"remote\",\"format\": \"binary\",\"url\": \"https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/category-ads-all.srs\",\"download_detour\": \"direct\"},\n {\"tag\": \"geoip-netflix\",\"type\": \"remote\",\"format\": \"binary\",\"url\": \"https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geoip/netflix.srs\",\"download_detour\": \"direct\"},\n {\"tag\": \"geosite-netflix\",\"type\": \"remote\",\"format\": \"binary\",\"url\": \"https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/netflix.srs\",\"download_detour\": \"direct\"},\n {\"tag\": \"geosite-disney\",\"type\": \"remote\",\"format\": \"binary\",\"url\": \"https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/disney.srs\",\"download_detour\": \"direct\"},\n {\"tag\": \"geosite-youtube\",\"type\": \"remote\",\"format\": \"binary\",\"url\": \"https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/youtube.srs\",\"download_detour\": \"direct\"},\n {\"tag\": \"geosite-max\",\"type\": \"remote\",\"format\": \"binary\",\"url\": \"https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/hbomax.srs\",\"download_detour\": \"direct\"},\n {\"tag\": \"geosite-spotify\",\"type\": \"remote\",\"format\": \"binary\",\"url\": \"https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/spotify.srs\",\"download_detour\": \"direct\"},\n {\"tag\": \"geoip-apple\",\"type\": \"remote\",\"format\": \"binary\",\"url\": \"https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo-lite/geoip/apple.srs\",\"download_detour\": \"direct\"},\n {\"tag\": \"geosite-apple\",\"type\": \"remote\",\"format\": \"binary\",\"url\": \"https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/apple.srs\",\"download_detour\": \"direct\"},\n {\"tag\": \"geoip-telegram\",\"type\": \"remote\",\"format\": \"binary\",\"url\": \"https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geoip/telegram.srs\",\"download_detour\": \"direct\"},\n {\"tag\": \"geosite-telegram\",\"type\": \"remote\",\"format\": \"binary\",\"url\": \"https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/telegram.srs\",\"download_detour\": \"direct\"},\n {\"tag\": \"geosite-openai\",\"type\": \"remote\",\"format\": \"binary\",\"url\": \"https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/openai.srs\",\"download_detour\": \"direct\"},\n {\"tag\": \"geosite-microsoft\",\"type\": \"remote\",\"format\": \"binary\",\"url\": \"https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/microsoft.srs\",\"download_detour\": \"direct\"},\n {\"tag\": \"geosite-tiktok\",\"type\": \"remote\",\"format\": \"binary\",\"url\": \"https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/tiktok.srs\",\"download_detour\": \"direct\"}\n ]\n }\n}\n', 'json', '{}', '2025-08-12 23:30:10.016', '2025-08-15 22:01:10.801'); +INSERT INTO `subscribe_application` (`id`, `name`, `icon`, `description`, `scheme`, `user_agent`, `is_default`, `subscribe_template`, `output_format`, `download_link`, `created_at`, `updated_at`) VALUES (5, 'Surge', '', '', 'surge:///install-config?url=${encodeURIComponent(url)}', 'Surge', 0, '{{- $GiB := 1073741824.0 -}}\n{{- $used := printf \"%.2f\" (divf (add (.UserInfo.Download | default 0 | float64) (.UserInfo.Upload | default 0 | float64)) $GiB) -}}\n{{- $traffic := (.UserInfo.Traffic | default 0 | float64) -}}\n{{- $total := printf \"%.2f\" (divf $traffic $GiB) -}}\n\n{{- $exp := \"\" -}}\n{{- $expStr := printf \"%v\" .UserInfo.ExpiredAt -}}\n{{- if regexMatch `^[0-9]+$` $expStr -}}\n {{- $ts := $expStr | float64 -}}\n {{- $sec := ternary (divf $ts 1000.0) $ts (ge (len $expStr) 13) -}}\n {{- $exp = (date \"2006-01-02 15:04:05\" (unixEpoch ($sec | int64))) -}}\n{{- else -}}\n {{- $exp = $expStr -}}\n{{- end -}}\n\n{{- $supportedProxies := list -}}\n{{- range $proxy := .Proxies -}}\n {{- if or (eq $proxy.Type \"shadowsocks\") (eq $proxy.Type \"vmess\") (eq $proxy.Type \"trojan\") (eq $proxy.Type \"hysteria2\") (eq $proxy.Type \"tuic\") -}}\n {{- $supportedProxies = append $supportedProxies $proxy -}}\n {{- end -}}\n{{- end -}}\n\n{{- $proxyNames := \"\" -}}\n{{- range $proxy := $supportedProxies -}}\n {{- if eq $proxyNames \"\" -}}\n {{- $proxyNames = $proxy.Name -}}\n {{- else -}}\n {{- $proxyNames = printf \"%s, %s\" $proxyNames $proxy.Name -}}\n {{- end -}}\n{{- end -}}\n\n#!MANAGED-CONFIG {{ .UserInfo.SubscribeURL }} interval=86400\n\n[General]\nloglevel = notify\nexternal-controller-access = perlnk@0.0.0.0:6170\nexclude-simple-hostnames = true\nshow-error-page-for-reject = true\nudp-priority = true\nudp-policy-not-supported-behaviour = reject\nipv6 = true\nipv6-vif = auto\nproxy-test-url = http://www.gstatic.com/generate_204\ninternet-test-url = http://connectivitycheck.platform.hicloud.com/generate_204\ntest-timeout = 5\ndns-server = system, 119.29.29.29, 223.5.5.5\nencrypted-dns-server = https://dns.alidns.com/dns-query\nhijack-dns = 8.8.8.8:53, 8.8.4.4:53, 1.1.1.1:53, 1.0.0.1:53\nskip-proxy = 192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12, 127.0.0.0/8, localhost, *.local\nalways-real-ip = *.lan, lens.l.google.com, *.srv.nintendo.net, *.stun.playstation.net, *.xboxlive.com, xbox.*.*.microsoft.com, *.msftncsi.com, *.msftconnecttest.com\n\n# > Surge Mac Parameters\nhttp-listen = 0.0.0.0:6088\nsocks5-listen = 0.0.0.0:6089\n\n# > Surge iOS Parameters\nallow-wifi-access = true\nallow-hotspot-access = true\nwifi-access-http-port = 6088\nwifi-access-socks5-port = 6089\n\n[Panel]\nSubscribeInfo = title={{ .SiteName }} - {{ .SubscribeName }}, content=官方网站: perlnk.com \\n已用流量: {{ $used }} GiB/{{ $total }} GiB \\n到期时间: {{ $exp }}, style=info\n\n[Proxy]\n{{- range $proxy := $supportedProxies }}\n {{- $server := $proxy.Server -}}\n {{- if and (contains $proxy.Server \":\") (not (hasPrefix \"[\" $proxy.Server)) -}}\n {{- $server = printf \"[%s]\" $proxy.Server -}}\n {{- end -}}\n\n {{- $sni := default \"\" $proxy.SNI -}}\n {{- if eq $sni \"\" -}}\n {{- $sni = default \"\" $proxy.Host -}}\n {{- end -}}\n {{- if and (eq $sni \"\") (not (or (regexMatch \"^[0-9]+\\\\.[0-9]+\\\\.[0-9]+\\\\.[0-9]+$\" $proxy.Server) (contains $proxy.Server \":\"))) -}}\n {{- $sni = $proxy.Server -}}\n {{- end -}}\n\n {{- $password := $.UserInfo.Password -}}\n {{- if and (eq $proxy.Type \"shadowsocks\") (ne (default \"\" $proxy.ServerKey) \"\") -}}\n {{- $method := $proxy.Method -}}\n {{- if or (hasPrefix \"2022-blake3-\" $method) (eq $method \"2022-blake3-aes-128-gcm\") (eq $method \"2022-blake3-aes-256-gcm\") -}}\n {{- $userKeyLen := ternary 16 32 (hasSuffix \"128-gcm\" $method) -}}\n {{- $pwdStr := printf \"%s\" $password -}}\n {{- $userKey := ternary $pwdStr (trunc $userKeyLen $pwdStr) (le (len $pwdStr) $userKeyLen) -}}\n {{- $serverB64 := b64enc $proxy.ServerKey -}}\n {{- $userB64 := b64enc $userKey -}}\n {{- $password = printf \"%s:%s\" $serverB64 $userB64 -}}\n {{- end -}}\n {{- end -}}\n\n {{- $common := \"udp-relay=true, tfo=true\" -}}\n\n {{- if eq $proxy.Type \"shadowsocks\" }}\n{{ $proxy.Name }} = ss, {{ $server }}, {{ $proxy.Port }}, encrypt-method={{ default \"aes-128-gcm\" $proxy.Method }}, password={{ $password }}{{- if ne (default \"\" $proxy.Transport) \"\" }}, obfs={{ $proxy.Transport }}, obfs-host={{ $sni }}{{- end }}, {{ $common }}\n {{- else if eq $proxy.Type \"vmess\" }}\n{{ $proxy.Name }} = vmess, {{ $server }}, {{ $proxy.Port }}, username={{ $password }}{{- if or (eq $proxy.Transport \"ws\") (eq $proxy.Transport \"websocket\") }}, ws=true{{- if ne (default \"\" $proxy.Path) \"\" }}, ws-path={{ $proxy.Path }}{{- end }}{{- if ne (default \"\" $proxy.Host) \"\" }}, ws-headers=\"Host:{{ $proxy.Host }}\"{{- end }}{{- else if eq $proxy.Transport \"grpc\" }}, grpc=true{{- if ne (default \"\" $proxy.ServiceName) \"\" }}, grpc-service-name={{ $proxy.ServiceName }}{{- end }}{{- end }}{{- if or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\") }}, tls=true{{- end }}{{- if ne $sni \"\" }}, sni={{ $sni }}{{- end }}{{- if $proxy.AllowInsecure }}, skip-cert-verify=true{{- end }}{{- if ne (default \"\" $proxy.Fingerprint) \"\" }}, fingerprint={{ $proxy.Fingerprint }}{{- end }}, {{ $common }}\n {{- else if eq $proxy.Type \"vless\" }}\n{{ $proxy.Name }} = vless, {{ $server }}, {{ $proxy.Port }}, username={{ $password }}{{- if or (eq $proxy.Transport \"ws\") (eq $proxy.Transport \"websocket\") }}, ws=true{{- if ne (default \"\" $proxy.Path) \"\" }}, ws-path={{ $proxy.Path }}{{- end }}{{- if ne (default \"\" $proxy.Host) \"\" }}, ws-headers=\"Host:{{ $proxy.Host }}\"{{- end }}{{- else if eq $proxy.Transport \"grpc\" }}, grpc=true{{- if ne (default \"\" $proxy.ServiceName) \"\" }}, grpc-service-name={{ $proxy.ServiceName }}{{- end }}{{- end }}{{- if ne $sni \"\" }}, sni={{ $sni }}{{- end }}{{- if $proxy.AllowInsecure }}, skip-cert-verify=true{{- end }}{{- if ne (default \"\" $proxy.Flow) \"\" }}, flow={{ $proxy.Flow }}{{- end }}, {{ $common }}\n {{- else if eq $proxy.Type \"trojan\" }}\n{{ $proxy.Name }} = trojan, {{ $server }}, {{ $proxy.Port }}, password={{ $password }}{{- if or (eq $proxy.Transport \"ws\") (eq $proxy.Transport \"websocket\") }}, ws=true{{- if ne (default \"\" $proxy.Path) \"\" }}, ws-path={{ $proxy.Path }}{{- end }}{{- if ne (default \"\" $proxy.Host) \"\" }}, ws-headers=\"Host:{{ $proxy.Host }}\"{{- end }}{{- else if eq $proxy.Transport \"grpc\" }}, grpc=true{{- if ne (default \"\" $proxy.ServiceName) \"\" }}, grpc-service-name={{ $proxy.ServiceName }}{{- end }}{{- end }}{{- if ne $sni \"\" }}, sni={{ $sni }}{{- end }}{{- if $proxy.AllowInsecure }}, skip-cert-verify=true{{- end }}{{- if ne (default \"\" $proxy.Fingerprint) \"\" }}, fingerprint={{ $proxy.Fingerprint }}{{- end }}, {{ $common }}\n {{- else if or (eq $proxy.Type \"hysteria2\") (eq $proxy.Type \"hy2\") }}\n{{ $proxy.Name }} = hysteria2, {{ $server }}, {{ $proxy.Port }}, password={{ $password }}{{- if ne $sni \"\" }}, sni={{ $sni }}{{- end }}{{- if $proxy.AllowInsecure }}, skip-cert-verify=true{{- end }}{{- if ne (default \"\" $proxy.ObfsPassword) \"\" }}, obfs=salamander, obfs-password={{ $proxy.ObfsPassword }}{{- end }}{{- if ne (default \"\" $proxy.HopPorts) \"\" }}, ports={{ $proxy.HopPorts }}{{- end }}{{- if ne (default 0 $proxy.HopInterval) 0 }}, hop-interval={{ $proxy.HopInterval }}{{- end }}, {{ $common }}\n {{- else if eq $proxy.Type \"tuic\" }}\n{{ $proxy.Name }} = tuic, {{ $server }}, {{ $proxy.Port }}, uuid={{ default \"\" $proxy.ServerKey }}, password={{ $password }}{{- if ne $sni \"\" }}, sni={{ $sni }}{{- end }}{{- if $proxy.AllowInsecure }}, skip-cert-verify=true{{- end }}{{- if $proxy.DisableSNI }}, disable-sni=true{{- end }}{{- if $proxy.ReduceRtt }}, reduce-rtt=true{{- end }}{{- if ne (default \"\" $proxy.UDPRelayMode) \"\" }}, udp-relay-mode={{ $proxy.UDPRelayMode }}{{- end }}{{- if ne (default \"\" $proxy.CongestionController) \"\" }}, congestion-controller={{ $proxy.CongestionController }}{{- end }}, {{ $common }}\n {{- else if eq $proxy.Type \"wireguard\" }}\n{{ $proxy.Name }} = wireguard, {{ $server }}, {{ $proxy.Port }}, private-key={{ default \"\" $proxy.ServerKey }}, public-key={{ default \"\" $proxy.RealityPublicKey }}{{- if ne (default \"\" $proxy.Path) \"\" }}, preshared-key={{ $proxy.Path }}{{- end }}{{- if ne (default \"\" $proxy.RealityServerAddr) \"\" }}, ip={{ $proxy.RealityServerAddr }}{{- end }}{{- if ne (default 0 $proxy.RealityServerPort) 0 }}, ipv6={{ $proxy.RealityServerPort }}{{- end }}, {{ $common }}\n {{- else if eq $proxy.Type \"anytls\" }}\n{{ $proxy.Name }} = anytls, {{ $server }}, {{ $proxy.Port }}, password={{ $password }}{{- if ne $sni \"\" }}, sni={{ $sni }}{{- end }}{{- if $proxy.AllowInsecure }}, skip-cert-verify=true{{- end }}, {{ $common }}\n {{- else }}\n{{ $proxy.Name }} = {{ $proxy.Type }}, {{ $server }}, {{ $proxy.Port }}, {{ $common }}\n {{- end }}\n{{- end }}\n\n[Proxy Group]\n🚀 Proxy = select, 🌏 Auto, 🎯 Direct, include-other-group=🇺🇳 Nodes\n🍎 Apple = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes\n🔍 Google = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes\n🪟 Microsoft = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes\n📺 GlobalMedia = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes\n🤖 AI = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes\n🪙 Crypto = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes\n🎮 Game = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes\n📟 Telegram = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes\n🇨🇳 China = select, 🎯 Direct, 🚀 Proxy, include-other-group=🇺🇳 Nodes\n🐠 Final = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes\n🌏 Auto = smart, include-other-group=🇺🇳 Nodes\n🎯 Direct = select, DIRECT, hidden=1\n🇺🇳 Nodes = select, {{ $proxyNames }}, hidden=1\n\n[Rule]\nRULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Apple/Apple_All.list, 🍎 Apple\nRULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Google/Google.list, 🔍 Google\nRULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/GitHub/GitHub.list, 🪟 Microsoft\nRULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Microsoft/Microsoft.list, 🪟 Microsoft\nRULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/HBO/HBO.list, 📺 GlobalMedia\nRULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Disney/Disney.list, 📺 GlobalMedia\nRULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/TikTok/TikTok.list, 📺 GlobalMedia\nRULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Netflix/Netflix.list, 📺 GlobalMedia\nRULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/GlobalMedia/GlobalMedia_All_No_Resolve.list, 📺 GlobalMedia\nRULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Telegram/Telegram.list, 📟 Telegram\nRULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/OpenAI/OpenAI.list, 🤖 AI\nRULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Gemini/Gemini.list, 🤖 AI\nRULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Copilot/Copilot.list, 🤖 AI\nRULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Claude/Claude.list, 🤖 AI\nRULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Crypto/Crypto.list, 🪙 Crypto\nRULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Cryptocurrency/Cryptocurrency.list, 🪙 Crypto\nRULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Game/Game.list, 🎮 Game\nRULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Global/Global_All_No_Resolve.list, 🚀 Proxy\nRULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/ChinaMax/ChinaMax_All_No_Resolve.list, 🇨🇳 China\nRULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Lan/Lan.list, 🎯 Direct\n\nGEOIP, CN, 🇨🇳 China\nFINAL, 🐠 Final, dns-failed\n\n[URL Rewrite]\n^https?:\\/\\/(www.)?g\\.cn https://www.google.com 302\n^https?:\\/\\/(www.)?google\\.cn https://www.google.com 302\n\n{{- range $proxy := $supportedProxies }}\n {{- if not (or (eq $proxy.Type \"shadowsocks\") (eq $proxy.Type \"vmess\") (eq $proxy.Type \"trojan\") (eq $proxy.Type \"hysteria2\") (eq $proxy.Type \"tuic\")) }}\n# Skipped (unsupported by Surge): {{ $proxy.Name }} ({{ $proxy.Type }})\n {{- end }}\n{{- end }}', 'conf', '{}', '2025-08-13 00:12:37.809', '2025-08-15 22:00:50.528'); +COMMIT; diff --git a/initialize/migrate/database/02102_subscribe_config.down.sql b/initialize/migrate/database/02102_subscribe_config.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/initialize/migrate/database/02102_subscribe_config.up.sql b/initialize/migrate/database/02102_subscribe_config.up.sql new file mode 100644 index 0000000..4460100 --- /dev/null +++ b/initialize/migrate/database/02102_subscribe_config.up.sql @@ -0,0 +1,4 @@ +INSERT IGNORE INTO `system` (`id`, `category`, `key`, `value`, `type`, `desc`, `created_at`, `updated_at`) +VALUES + (42, 'subscribe', 'UserAgentLimit', 'false', 'bool', 'User Agent Limit', '2025-04-22 14:25:16.637', '2025-04-22 14:25:16.637'), + (43, 'subscribe', 'UserAgentList', '', 'string', 'User Agent List', '2025-04-22 14:25:16.637','2025-04-22 14:25:16.637'); \ No newline at end of file diff --git a/initialize/migrate/database/02103_delete_application.down.sql b/initialize/migrate/database/02103_delete_application.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/initialize/migrate/database/02103_delete_application.up.sql b/initialize/migrate/database/02103_delete_application.up.sql new file mode 100644 index 0000000..1a3a778 --- /dev/null +++ b/initialize/migrate/database/02103_delete_application.up.sql @@ -0,0 +1,3 @@ +DROP TABLE IF EXISTS `application`; +DROP TABLE IF EXISTS `application_version`; +DROP TABLE IF EXISTS `application_config`; \ No newline at end of file diff --git a/initialize/migrate/database/02104_system_log.down.sql b/initialize/migrate/database/02104_system_log.down.sql new file mode 100644 index 0000000..2682c97 --- /dev/null +++ b/initialize/migrate/database/02104_system_log.down.sql @@ -0,0 +1,106 @@ +CREATE TABLE IF NOT EXISTS `user_balance_log` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT 'User ID', + `amount` bigint NOT NULL COMMENT 'Amount', + `type` tinyint(1) NOT NULL COMMENT 'Type: 1: Recharge 2: Withdraw 3: Payment 4: Refund 5: Reward', + `order_id` bigint DEFAULT NULL COMMENT 'Order ID', + `balance` bigint NOT NULL COMMENT 'Balance', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`) + ) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `user_commission_log` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT 'User ID', + `order_no` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Order No.', + `amount` bigint NOT NULL COMMENT 'Amount', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`) + ) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `user_gift_amount_log` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT 'User ID', + `user_subscribe_id` bigint DEFAULT NULL COMMENT 'Deduction User Subscribe ID', + `order_no` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Order No.', + `type` tinyint(1) NOT NULL COMMENT 'Type: 1: Increase 2: Reduce', + `amount` bigint NOT NULL COMMENT 'Amount', + `balance` bigint NOT NULL COMMENT 'Balance', + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Remark', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`) + ) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `user_login_log` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT 'User ID', + `login_ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Login IP', + `user_agent` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'UserAgent', + `success` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Login Success', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`) + ) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `user_reset_subscribe_log` +( + `id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `user_id` BIGINT NOT NULL COMMENT 'User ID', + `type` TINYINT(1) NOT NULL COMMENT 'Type: 1: Auto 2: Advance 3: Paid', + `order_no` VARCHAR(255) DEFAULT NULL COMMENT 'Order No.', + `user_subscribe_id` BIGINT NOT NULL COMMENT 'User Subscribe ID', + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Creation Time', + INDEX `idx_user_id` (`user_id`), + INDEX `idx_user_subscribe_id` (`user_subscribe_id`) + ) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `user_subscribe_log` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT 'User ID', + `user_subscribe_id` bigint NOT NULL COMMENT 'User Subscribe ID', + `token` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Token', + `ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'IP', + `user_agent` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'UserAgent', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_user_subscribe_id` (`user_subscribe_id`) + ) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `message_log` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'email' COMMENT 'Message Type', + `platform` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'smtp' COMMENT 'Platform', + `to` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'To', + `subject` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Subject', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Content', + `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Status', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`) + ) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +DROP TABLE IF EXISTS `system_logs`; \ No newline at end of file diff --git a/initialize/migrate/database/02104_system_log.up.sql b/initialize/migrate/database/02104_system_log.up.sql new file mode 100644 index 0000000..b518e68 --- /dev/null +++ b/initialize/migrate/database/02104_system_log.up.sql @@ -0,0 +1,19 @@ +DROP TABLE IF EXISTS `user_balance_log`; +DROP TABLE IF EXISTS `user_commission_log`; +DROP TABLE IF EXISTS `user_gift_amount_log`; +DROP TABLE IF EXISTS `user_login_log`; +DROP TABLE IF EXISTS `user_reset_subscribe_log`; +DROP TABLE IF EXISTS `user_subscribe_log`; +DROP TABLE IF EXISTS `message_log`; +DROP TABLE IF EXISTS `system_logs`; +CREATE TABLE `system_logs` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `type` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Log Type: 1: Email Message 2: Mobile Message 3: Subscribe 4: Subscribe Traffic 5: Server Traffic 6: Login 7: Register 8: Balance 9: Commission 10: Reset Subscribe 11: Gift', + `date` varchar(20) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Log Date', + `object_id` bigint NOT NULL DEFAULT '0' COMMENT 'Object ID', + `content` text COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Log Content', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', + PRIMARY KEY (`id`), + KEY `idx_type` (`type`), + KEY `idx_object_id` (`object_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; \ No newline at end of file diff --git a/initialize/migrate/database/02105_node.down.sql b/initialize/migrate/database/02105_node.down.sql new file mode 100644 index 0000000..210462e --- /dev/null +++ b/initialize/migrate/database/02105_node.down.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS `nodes`; +DROP TABLE IF EXISTS `servers`; diff --git a/initialize/migrate/database/02105_node.up.sql b/initialize/migrate/database/02105_node.up.sql new file mode 100644 index 0000000..c9c310b --- /dev/null +++ b/initialize/migrate/database/02105_node.up.sql @@ -0,0 +1,28 @@ +CREATE TABLE IF NOT EXISTS `servers` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(100) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Server Name', + `country` varchar(128) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Country', + `city` varchar(128) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'City', + `ratio` decimal(4,2) NOT NULL DEFAULT '0.00' COMMENT 'Traffic Ratio', + `address` varchar(100) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Server Address', + `sort` bigint NOT NULL DEFAULT '0' COMMENT 'Sort', + `protocols` text COLLATE utf8mb4_general_ci COMMENT 'Protocol', + `last_reported_at` datetime(3) DEFAULT NULL COMMENT 'Last Reported Time', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `nodes` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(100) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Node Name', + `tags` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Tags', + `port` smallint unsigned NOT NULL DEFAULT '0' COMMENT 'Connect Port', + `address` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Connect Address', + `server_id` bigint NOT NULL DEFAULT '0' COMMENT 'Server ID', + `protocol` varchar(100) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Protocol', + `enabled` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Enabled', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; diff --git a/initialize/migrate/database/02106_subscribe.down.sql b/initialize/migrate/database/02106_subscribe.down.sql new file mode 100644 index 0000000..20984b7 --- /dev/null +++ b/initialize/migrate/database/02106_subscribe.down.sql @@ -0,0 +1,5 @@ +ALTER TABLE `subscribe` +DROP COLUMN `nodes`, + DROP COLUMN `node_tags`, + ADD COLUMN `server` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'Server', + ADD COLUMN `server_group` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'Server Group'; diff --git a/initialize/migrate/database/02106_subscribe.up.sql b/initialize/migrate/database/02106_subscribe.up.sql new file mode 100644 index 0000000..28f5db5 --- /dev/null +++ b/initialize/migrate/database/02106_subscribe.up.sql @@ -0,0 +1,7 @@ +ALTER TABLE `subscribe` +ADD COLUMN `nodes` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'Node IDs', +ADD COLUMN `node_tags` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'Node Tags', +DROP COLUMN `server`, +DROP COLUMN `server_group`; + +DROP TABLE IF EXISTS `server_rule_group`; diff --git a/initialize/migrate/database/02107_log_setting.down.sql b/initialize/migrate/database/02107_log_setting.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/initialize/migrate/database/02107_log_setting.up.sql b/initialize/migrate/database/02107_log_setting.up.sql new file mode 100644 index 0000000..c1bc8e2 --- /dev/null +++ b/initialize/migrate/database/02107_log_setting.up.sql @@ -0,0 +1,4 @@ +INSERT IGNORE INTO `system` (`category`, `key`, `value`, `type`, `desc`, `created_at`, `updated_at`) +VALUES + ('log', 'AutoClear', 'true', 'bool', 'Auto Clear Log', '2025-04-22 14:25:16.637', '2025-04-22 14:25:16.637'), + ('log', 'ClearDays', '7', 'int', 'Clear Days', '2025-04-22 14:25:16.637','2025-04-22 14:25:16.637'); \ No newline at end of file diff --git a/initialize/migrate/database/02108_user_referral.down.sql b/initialize/migrate/database/02108_user_referral.down.sql new file mode 100644 index 0000000..3bdac5b --- /dev/null +++ b/initialize/migrate/database/02108_user_referral.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE `user` +DROP COLUMN `referral_percentage`, +DROP COLUMN `only_first_purchase`; \ No newline at end of file diff --git a/initialize/migrate/database/02108_user_referral.up.sql b/initialize/migrate/database/02108_user_referral.up.sql new file mode 100644 index 0000000..e50f765 --- /dev/null +++ b/initialize/migrate/database/02108_user_referral.up.sql @@ -0,0 +1,7 @@ +ALTER TABLE `user` + ADD COLUMN `referral_percentage` TINYINT UNSIGNED NOT NULL DEFAULT 0 + COMMENT 'Referral Percentage' + AFTER `commission`, + ADD COLUMN `only_first_purchase` TINYINT(1) NOT NULL DEFAULT 1 + COMMENT 'Only First Purchase' + AFTER `referral_percentage`; diff --git a/initialize/migrate/database/02109_node_sort.down.sql b/initialize/migrate/database/02109_node_sort.down.sql new file mode 100644 index 0000000..4413646 --- /dev/null +++ b/initialize/migrate/database/02109_node_sort.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE `nodes` +DROP COLUMN `sort`; \ No newline at end of file diff --git a/initialize/migrate/database/02109_node_sort.up.sql b/initialize/migrate/database/02109_node_sort.up.sql new file mode 100644 index 0000000..1a993d0 --- /dev/null +++ b/initialize/migrate/database/02109_node_sort.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE `nodes` + ADD COLUMN `sort` INT UNSIGNED NOT NULL DEFAULT 0 + COMMENT 'Sort' AFTER `enabled`; \ No newline at end of file diff --git a/initialize/migrate/database/02110_traffic_log_index.down.sql b/initialize/migrate/database/02110_traffic_log_index.down.sql new file mode 100644 index 0000000..f42807c --- /dev/null +++ b/initialize/migrate/database/02110_traffic_log_index.down.sql @@ -0,0 +1 @@ +DROP INDEX idx_traffic_log_time_user_sub ON traffic_log; diff --git a/initialize/migrate/database/02110_traffic_log_index.up.sql b/initialize/migrate/database/02110_traffic_log_index.up.sql new file mode 100644 index 0000000..2cf61f2 --- /dev/null +++ b/initialize/migrate/database/02110_traffic_log_index.up.sql @@ -0,0 +1 @@ +CREATE INDEX idx_traffic_log_time_user_sub ON traffic_log (timestamp, user_id, subscribe_id); diff --git a/initialize/migrate/database/02111_clear_table.down.sql b/initialize/migrate/database/02111_clear_table.down.sql new file mode 100644 index 0000000..85c9f3f --- /dev/null +++ b/initialize/migrate/database/02111_clear_table.down.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS `subscribe_type`; +DROP TABLE IF EXISTS `sms`; \ No newline at end of file diff --git a/initialize/migrate/database/02111_clear_table.up.sql b/initialize/migrate/database/02111_clear_table.up.sql new file mode 100644 index 0000000..85c9f3f --- /dev/null +++ b/initialize/migrate/database/02111_clear_table.up.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS `subscribe_type`; +DROP TABLE IF EXISTS `sms`; \ No newline at end of file diff --git a/initialize/migrate/database/02112_subscribe.down.sql b/initialize/migrate/database/02112_subscribe.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/initialize/migrate/database/02112_subscribe.up.sql b/initialize/migrate/database/02112_subscribe.up.sql new file mode 100644 index 0000000..1a79dbc --- /dev/null +++ b/initialize/migrate/database/02112_subscribe.up.sql @@ -0,0 +1,7 @@ +ALTER TABLE `subscribe` +DROP COLUMN `group_id`, +ADD COLUMN `language` VARCHAR(255) NOT NULL DEFAULT '' + COMMENT 'Language' + AFTER `name`; + +DROP TABLE IF EXISTS `subscribe_group`; \ No newline at end of file diff --git a/initialize/migrate/database/02113_task.down.sql b/initialize/migrate/database/02113_task.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/initialize/migrate/database/02113_task.up.sql b/initialize/migrate/database/02113_task.up.sql new file mode 100644 index 0000000..4e7b170 --- /dev/null +++ b/initialize/migrate/database/02113_task.up.sql @@ -0,0 +1,14 @@ +DROP TABLE IF EXISTS `email_task`; +CREATE TABLE `task` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID', + `type` tinyint NOT NULL COMMENT 'Task Type', + `scope` text COLLATE utf8mb4_general_ci COMMENT 'Task Scope', + `content` text COLLATE utf8mb4_general_ci COMMENT 'Task Content', + `status` tinyint NOT NULL DEFAULT '0' COMMENT 'Task Status: 0: Pending, 1: In Progress, 2: Completed, 3: Failed', + `errors` text COLLATE utf8mb4_general_ci COMMENT 'Task Errors', + `total` bigint unsigned NOT NULL DEFAULT '0' COMMENT 'Total Number', + `current` bigint unsigned NOT NULL DEFAULT '0' COMMENT 'Current Number', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; \ No newline at end of file diff --git a/initialize/migrate/database/02114_node_config.down.sql b/initialize/migrate/database/02114_node_config.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/initialize/migrate/database/02114_node_config.up.sql b/initialize/migrate/database/02114_node_config.up.sql new file mode 100644 index 0000000..1289647 --- /dev/null +++ b/initialize/migrate/database/02114_node_config.up.sql @@ -0,0 +1,8 @@ +INSERT +IGNORE INTO `system` (`category`, `key`, `value`, `type`, `desc`, `created_at`, `updated_at`) +VALUE + ('server', 'TrafficReportThreshold', '0', 'int', 'Traffic report threshold', '2025-04-22 14:25:16.637','2025-04-22 14:25:16.637'), + ('server', 'IPStrategy', '', 'string', 'IP Strategy', '2025-04-22 14:25:16.637','2025-04-22 14:25:16.637'), + ('server', 'DNS', '', 'string', 'DNS', '2025-04-22 14:25:16.637','2025-04-22 14:25:16.637'), + ('server', 'Block', '', 'string', 'Block', '2025-04-22 14:25:16.637','2025-04-22 14:25:16.637'), + ('server', 'Outbound', '', 'string', 'Proxy Outbound', '2025-04-22 14:25:16.637','2025-04-22 14:25:16.637'); \ No newline at end of file diff --git a/initialize/migrate/database/02115_ads.down.sql b/initialize/migrate/database/02115_ads.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/initialize/migrate/database/02115_ads.up.sql b/initialize/migrate/database/02115_ads.up.sql new file mode 100644 index 0000000..39cfaf2 --- /dev/null +++ b/initialize/migrate/database/02115_ads.up.sql @@ -0,0 +1,20 @@ +-- 只有当 ads 表中不存在 description 字段时才添加 +SET +@col_exists := ( + SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'ads' + AND COLUMN_NAME = 'description' +); + +SET +@query := IF( + @col_exists = 0, + 'ALTER TABLE `ads` ADD COLUMN `description` VARCHAR(255) DEFAULT '''' COMMENT ''Description'';', + 'SELECT "Column `description` already exists"' +); + +PREPARE stmt FROM @query; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; diff --git a/initialize/migrate/database/02116_user_algo.down.sql b/initialize/migrate/database/02116_user_algo.down.sql new file mode 100644 index 0000000..41644c2 --- /dev/null +++ b/initialize/migrate/database/02116_user_algo.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE `user` +DROP COLUMN `algo`, + DROP COLUMN `salt`; diff --git a/initialize/migrate/database/02116_user_algo.up.sql b/initialize/migrate/database/02116_user_algo.up.sql new file mode 100644 index 0000000..4a79ef2 --- /dev/null +++ b/initialize/migrate/database/02116_user_algo.up.sql @@ -0,0 +1,35 @@ +-- 添加 algo 列(如果不存在) +SET @dbname = DATABASE(); +SET @tablename = 'user'; +SET @colname = 'algo'; +SET @sql = ( + SELECT IF( + COUNT(*) = 0, + 'ALTER TABLE `user` ADD COLUMN `algo` VARCHAR(20) NOT NULL DEFAULT ''default'' COMMENT ''Encryption Algorithm'' AFTER `password`;', + 'SELECT "Column `algo` already exists";' + ) + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @dbname + AND TABLE_NAME = @tablename + AND COLUMN_NAME = @colname +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- 添加 salt 列(如果不存在) +SET @colname = 'salt'; +SET @sql = ( + SELECT IF( + COUNT(*) = 0, + 'ALTER TABLE `user` ADD COLUMN `salt` VARCHAR(20) NOT NULL DEFAULT ''default'' COMMENT ''Password Salt'' AFTER `algo`;', + 'SELECT "Column `salt` already exists";' + ) + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @dbname + AND TABLE_NAME = @tablename + AND COLUMN_NAME = @colname +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; diff --git a/initialize/migrate/database/02117_site_custom_data.down.sql b/initialize/migrate/database/02117_site_custom_data.down.sql new file mode 100644 index 0000000..c8581e8 --- /dev/null +++ b/initialize/migrate/database/02117_site_custom_data.down.sql @@ -0,0 +1,7 @@ +INSERT INTO `system` (`category`, `key`, `value`, `type`, `desc`, `created_at`, `updated_at`) +SELECT 'site', 'CustomData', '{ + "kr_website_id": "" +}', 'string', 'Custom Data', '2025-04-22 14:25:16.637', '2025-10-14 15:47:19.187' + WHERE NOT EXISTS ( + SELECT 1 FROM `system` WHERE `category` = 'site' AND `key` = 'CustomData' +); diff --git a/initialize/migrate/database/02117_site_custom_data.up.sql b/initialize/migrate/database/02117_site_custom_data.up.sql new file mode 100644 index 0000000..c8581e8 --- /dev/null +++ b/initialize/migrate/database/02117_site_custom_data.up.sql @@ -0,0 +1,7 @@ +INSERT INTO `system` (`category`, `key`, `value`, `type`, `desc`, `created_at`, `updated_at`) +SELECT 'site', 'CustomData', '{ + "kr_website_id": "" +}', 'string', 'Custom Data', '2025-04-22 14:25:16.637', '2025-10-14 15:47:19.187' + WHERE NOT EXISTS ( + SELECT 1 FROM `system` WHERE `category` = 'site' AND `key` = 'CustomData' +); diff --git a/initialize/migrate/database/02118_traffic_log_idx.down.sql b/initialize/migrate/database/02118_traffic_log_idx.down.sql new file mode 100644 index 0000000..25598dd --- /dev/null +++ b/initialize/migrate/database/02118_traffic_log_idx.down.sql @@ -0,0 +1 @@ +ALTER TABLE traffic_log DROP INDEX idx_timestamp; diff --git a/initialize/migrate/database/02118_traffic_log_idx.up.sql b/initialize/migrate/database/02118_traffic_log_idx.up.sql new file mode 100644 index 0000000..cdd308f --- /dev/null +++ b/initialize/migrate/database/02118_traffic_log_idx.up.sql @@ -0,0 +1 @@ +ALTER TABLE traffic_log ADD INDEX idx_timestamp (timestamp); diff --git a/initialize/migrate/database/02119_user_subscribe_note.down.sql b/initialize/migrate/database/02119_user_subscribe_note.down.sql new file mode 100644 index 0000000..60cc0e8 --- /dev/null +++ b/initialize/migrate/database/02119_user_subscribe_note.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE `user_subscribe` +DROP COLUMN `note`; diff --git a/initialize/migrate/database/02119_user_subscribe_note.up.sql b/initialize/migrate/database/02119_user_subscribe_note.up.sql new file mode 100644 index 0000000..b8b6983 --- /dev/null +++ b/initialize/migrate/database/02119_user_subscribe_note.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE `user_subscribe` +ADD COLUMN `note` VARCHAR(500) NOT NULL DEFAULT '' + COMMENT 'User note for subscription' + AFTER `status`; diff --git a/initialize/migrate/database/02120_user_rules.down.sql b/initialize/migrate/database/02120_user_rules.down.sql new file mode 100644 index 0000000..718f4a6 --- /dev/null +++ b/initialize/migrate/database/02120_user_rules.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE `user` +DROP COLUMN IF EXISTS `rules`; diff --git a/initialize/migrate/database/02120_user_rules.up.sql b/initialize/migrate/database/02120_user_rules.up.sql new file mode 100644 index 0000000..5e93aca --- /dev/null +++ b/initialize/migrate/database/02120_user_rules.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE `user` + ADD COLUMN `rules` TEXT NULL + COMMENT 'User rules for subscription' + AFTER `created_at`; diff --git a/initialize/migrate/database/02121_user_withdrawal.down.sql b/initialize/migrate/database/02121_user_withdrawal.down.sql new file mode 100644 index 0000000..4de8bc5 --- /dev/null +++ b/initialize/migrate/database/02121_user_withdrawal.down.sql @@ -0,0 +1,5 @@ +DROP TABLE IF EXISTS `withdrawals`; + +DELETE FROM `system` +WHERE `category` = 'invite' + AND `key` = 'WithdrawalMethod'; diff --git a/initialize/migrate/database/02121_user_withdrawal.up.sql b/initialize/migrate/database/02121_user_withdrawal.up.sql new file mode 100644 index 0000000..4f39e1e --- /dev/null +++ b/initialize/migrate/database/02121_user_withdrawal.up.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS `withdrawals` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'Primary Key', + `user_id` BIGINT NOT NULL COMMENT 'User ID', + `amount` BIGINT NOT NULL COMMENT 'Withdrawal Amount', + `content` TEXT COMMENT 'Withdrawal Content', + `status` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Withdrawal Status', + `reason` VARCHAR(500) NOT NULL DEFAULT '' COMMENT 'Rejection Reason', + `created_at` DATETIME NOT NULL COMMENT 'Creation Time', + `updated_at` DATETIME NOT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +INSERT IGNORE INTO `system` (`category`, `key`, `value`, `type`, `desc`, `created_at`, `updated_at`) +VALUES + ('invite', 'WithdrawalMethod', '', 'string', 'withdrawal method', '2025-04-22 14:25:16.637', '2025-04-22 14:25:16.637'); \ No newline at end of file diff --git a/initialize/migrate/migrate_test.go b/initialize/migrate/migrate_test.go index c22e52b..531266e 100644 --- a/initialize/migrate/migrate_test.go +++ b/initialize/migrate/migrate_test.go @@ -3,7 +3,10 @@ package migrate import ( "testing" + "github.com/perfect-panel/server/internal/model/node" "github.com/perfect-panel/server/pkg/orm" + "gorm.io/driver/mysql" + "gorm.io/gorm" ) func getDSN() string { @@ -30,3 +33,17 @@ func TestMigrate(t *testing.T) { t.Log("migrate success") } } +func TestMysql(t *testing.T) { + db, err := gorm.Open(mysql.New(mysql.Config{ + DSN: "root:mylove520@tcp(localhost:3306)/vpnboard", + })) + if err != nil { + t.Fatalf("Failed to connect to MySQL: %v", err) + } + err = db.Migrator().AutoMigrate(&node.Node{}) + if err != nil { + t.Fatalf("Failed to auto migrate: %v", err) + return + } + t.Log("MySQL connection and migration successful") +} diff --git a/initialize/node.go b/initialize/node.go index da7c3fb..51dce08 100644 --- a/initialize/node.go +++ b/initialize/node.go @@ -3,7 +3,6 @@ package initialize import ( "context" "encoding/json" - "github.com/perfect-panel/server/pkg/logger" "github.com/perfect-panel/server/internal/config" @@ -19,9 +18,40 @@ func Node(ctx *svc.ServiceContext) { if err != nil { panic(err) } - var nodeConfig config.NodeConfig + var nodeConfig config.NodeDBConfig tool.SystemConfigSliceReflectToStruct(configs, &nodeConfig) - ctx.Config.Node = nodeConfig + c := config.NodeConfig{ + NodeSecret: nodeConfig.NodeSecret, + NodePullInterval: nodeConfig.NodePullInterval, + NodePushInterval: nodeConfig.NodePushInterval, + IPStrategy: nodeConfig.IPStrategy, + TrafficReportThreshold: nodeConfig.TrafficReportThreshold, + } + if nodeConfig.DNS != "" { + var dns []config.NodeDNS + err = json.Unmarshal([]byte(nodeConfig.DNS), &dns) + if err != nil { + logger.Errorf("[Node] Unmarshal DNS config error: %s", err.Error()) + panic(err) + } + c.DNS = dns + } + if nodeConfig.Block != "" { + var block []string + _ = json.Unmarshal([]byte(nodeConfig.Block), &block) + c.Block = tool.RemoveDuplicateElements(block...) + } + if nodeConfig.Outbound != "" { + var outbound []config.NodeOutbound + err = json.Unmarshal([]byte(nodeConfig.Outbound), &outbound) + if err != nil { + logger.Errorf("[Node] Unmarshal Outbound config error: %s", err.Error()) + panic(err) + } + c.Outbound = outbound + } + + ctx.Config.Node = c // Manager initialization if ctx.DB.Model(&system.System{}).Where("`key` = ?", "NodeMultiplierConfig").Find(&system.System{}).RowsAffected == 0 { @@ -39,7 +69,6 @@ func Node(ctx *svc.ServiceContext) { nodeMultiplierData, err := ctx.SystemModel.FindNodeMultiplierConfig(context.Background()) if err != nil { - logger.Error("Get Node Multiplier Config Error: ", logger.Field("error", err.Error())) return } diff --git a/initialize/statistics.go b/initialize/statistics.go deleted file mode 100644 index 7ab02ad..0000000 --- a/initialize/statistics.go +++ /dev/null @@ -1,57 +0,0 @@ -package initialize - -import ( - "context" - "time" - - "github.com/perfect-panel/server/internal/model/cache" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/pkg/logger" -) - -func TrafficDataToRedis(svcCtx *svc.ServiceContext) { - ctx := context.Background() - // 统计昨天的节点流量数据排行榜前10 - nodeData, err := svcCtx.TrafficLogModel.TopServersTrafficByDay(ctx, time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day()-1, 0, 0, 0, 0, time.Local), 10) - if err != nil { - logger.Errorw("统计昨天的流量数据失败", logger.Field("error", err.Error())) - } - var nodeCacheData []cache.NodeTodayTrafficRank - for _, node := range nodeData { - serverInfo, err := svcCtx.ServerModel.FindOne(ctx, node.ServerId) - if err != nil { - logger.Errorw("查询节点信息失败", logger.Field("error", err.Error())) - continue - } - nodeCacheData = append(nodeCacheData, cache.NodeTodayTrafficRank{ - ID: node.ServerId, - Name: serverInfo.Name, - Upload: node.Upload, - Download: node.Download, - Total: node.Upload + node.Download, - }) - } - // 写入缓存 - if err = svcCtx.NodeCache.UpdateYesterdayNodeTotalTrafficRank(ctx, nodeCacheData); err != nil { - logger.Errorw("写入昨天的流量数据到缓存失败", logger.Field("error", err.Error())) - } - // 统计昨天的用户流量数据排行榜前10 - userData, err := svcCtx.TrafficLogModel.TopUsersTrafficByDay(ctx, time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day()-1, 0, 0, 0, 0, time.Local), 10) - if err != nil { - logger.Errorw("统计昨天的流量数据失败", logger.Field("error", err.Error())) - } - var userCacheData []cache.UserTodayTrafficRank - for _, user := range userData { - userCacheData = append(userCacheData, cache.UserTodayTrafficRank{ - SID: user.SubscribeId, - Upload: user.Upload, - Download: user.Download, - Total: user.Upload + user.Download, - }) - } - // 写入缓存 - if err = svcCtx.NodeCache.UpdateYesterdayUserTotalTrafficRank(ctx, userCacheData); err != nil { - logger.Errorw("写入昨天的流量数据到缓存失败", logger.Field("error", err.Error())) - } - logger.Infow("初始化昨天的流量数据到缓存成功") -} diff --git a/initialize/version.go b/initialize/version.go index 9eac7d4..14d0875 100644 --- a/initialize/version.go +++ b/initialize/version.go @@ -2,6 +2,7 @@ package initialize import ( "errors" + "time" "github.com/perfect-panel/server/internal/model/user" "gorm.io/gorm" @@ -16,6 +17,7 @@ func Migrate(ctx *svc.ServiceContext) { mc := orm.Mysql{ Config: ctx.Config.MySQL, } + now := time.Now() if err := migrate.Migrate(mc.Dsn()).Up(); err != nil { if errors.Is(err, migrate.NoChange) { logger.Info("[Migrate] database not change") @@ -23,6 +25,8 @@ func Migrate(ctx *svc.ServiceContext) { } logger.Errorf("[Migrate] Up error: %v", err.Error()) panic(err) + } else { + logger.Info("[Migrate] Database change, took " + time.Since(now).String()) } // if not found admin user err := ctx.DB.Transaction(func(tx *gorm.DB) error { diff --git a/internal/config/cacheKey.go b/internal/config/cacheKey.go index 02f5be9..655ce55 100644 --- a/internal/config/cacheKey.go +++ b/internal/config/cacheKey.go @@ -12,9 +12,6 @@ const SiteConfigKey = "system:site_config" // SubscribeConfigKey Subscribe Config Key const SubscribeConfigKey = "system:subscribe_config" -// ApplicationKey Application Key -const ApplicationKey = "system:application" - // RegisterConfigKey Register Config Key const RegisterConfigKey = "system:register_config" @@ -51,26 +48,12 @@ const AuthCodeCacheKey = "auth:verify:email" // AuthCodeTelephoneCacheKey Register Code Cache Key const AuthCodeTelephoneCacheKey = "auth:verify:telephone" -// ServerUserListCacheKey Server User List Cache Key -const ServerUserListCacheKey = "server:user_list:id:" - -// ServerConfigCacheKey Server Config Cache Key -const ServerConfigCacheKey = "server:config:id:" - -// CommonStat Cache Key +// CommonStatCacheKey CommonStat Cache Key const CommonStatCacheKey = "common:stat" -// ServerStatusCacheKey Server Status Cache Key -const ServerStatusCacheKey = "server:status:id:" - // ServerCountCacheKey Server Count Cache Key const ServerCountCacheKey = "server:count" -// UserBindTelegramCacheKey User Bind Telegram Cache Key -const UserBindTelegramCacheKey = "user:bind:telegram:code:" - -const CacheSmsCount = "cache:sms:count" - // SendIntervalKeyPrefix Auth Code Send Interval Key Prefix const SendIntervalKeyPrefix = "send:interval:" diff --git a/internal/config/config.go b/internal/config/config.go index d56acfa..d582fb7 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,6 +1,8 @@ package config import ( + "encoding/json" + "github.com/perfect-panel/server/pkg/logger" "github.com/perfect-panel/server/pkg/orm" ) @@ -19,12 +21,14 @@ type Config struct { Node NodeConfig `yaml:"Node"` Mobile MobileConfig `yaml:"Mobile"` Email EmailConfig `yaml:"Email"` + Device DeviceConfig `yaml:"device"` Verify Verify `yaml:"Verify"` VerifyCode VerifyCode `yaml:"VerifyCode"` Register RegisterConfig `yaml:"Register"` Subscribe SubscribeConfig `yaml:"Subscribe"` Invite InviteConfig `yaml:"Invite"` Telegram Telegram `yaml:"Telegram"` + Log Log `yaml:"Log"` Administrator struct { Email string `yaml:"Email" default:"admin@ppanel.dev"` Password string `yaml:"Password" default:"password"` @@ -52,9 +56,11 @@ type Verify struct { type SubscribeConfig struct { SingleModel bool `yaml:"SingleModel" default:"false"` - SubscribePath string `yaml:"SubscribePath" default:"/api/subscribe"` + SubscribePath string `yaml:"SubscribePath" default:"/v1/subscribe/config"` SubscribeDomain string `yaml:"SubscribeDomain" default:""` PanDomain bool `yaml:"PanDomain" default:"false"` + UserAgentLimit bool `yaml:"UserAgentLimit" default:"false"` + UserAgentList string `yaml:"UserAgentList" default:""` } type RegisterConfig struct { @@ -91,6 +97,14 @@ type MobileConfig struct { Whitelist []string `yaml:"whitelist"` } +type DeviceConfig struct { + Enable bool `yaml:"enable" default:"true"` + ShowAds bool `yaml:"show_ads"` + EnableSecurity bool `yaml:"enable_security"` + OnlyRealDevice bool `yaml:"only_real_device"` + SecuritySecret string `yaml:"security_secret"` +} + type SiteConfig struct { Host string `yaml:"Host" default:""` SiteName string `yaml:"SiteName" default:""` @@ -102,9 +116,76 @@ type SiteConfig struct { } type NodeConfig struct { - NodeSecret string `yaml:"NodeSecret" default:""` - NodePullInterval int64 `yaml:"NodePullInterval" default:"60"` - NodePushInterval int64 `yaml:"NodePushInterval" default:"60"` + NodeSecret string `yaml:"NodeSecret" default:""` + NodePullInterval int64 `yaml:"NodePullInterval" default:"60"` + NodePushInterval int64 `yaml:"NodePushInterval" default:"60"` + TrafficReportThreshold int64 `yaml:"TrafficReportThreshold" default:"0"` + IPStrategy string `yaml:"IPStrategy" default:""` + DNS []NodeDNS `yaml:"DNS"` + Block []string `yaml:"Block" ` + Outbound []NodeOutbound `yaml:"Outbound"` +} + +func (n *NodeConfig) Marshal() ([]byte, error) { + type Alias NodeConfig + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(n), + }) +} + +func (n *NodeConfig) Unmarshal(data []byte) error { + type Alias NodeConfig + aux := &struct { + *Alias + }{ + Alias: (*Alias)(n), + } + return json.Unmarshal(data, &aux) +} + +type NodeDNS struct { + Proto string `json:"proto"` + Address string `json:"address"` + Domains []string `json:"domains"` +} + +func (n *NodeDNS) Marshal() ([]byte, error) { + type Alias NodeDNS + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(n), + }) +} + +func (n *NodeDNS) Unmarshal(data []byte) error { + type Alias NodeDNS + aux := &struct { + *Alias + }{ + Alias: (*Alias)(n), + } + return json.Unmarshal(data, &aux) +} + +type NodeOutbound struct { + Name string `json:"name"` + Protocol string `json:"protocol"` + Address string `json:"address"` + Port int64 `json:"port"` + Password string `json:"password"` + Rules []string `json:"rules"` +} + +func (n *NodeOutbound) Marshal() ([]byte, error) { + type Alias NodeOutbound + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(n), + }) } type File struct { @@ -145,3 +226,19 @@ type VerifyCode struct { Limit int64 `yaml:"Limit" default:"15"` Interval int64 `yaml:"Interval" default:"60"` } + +type Log struct { + AutoClear bool `yaml:"AutoClear" default:"true"` + ClearDays int64 `yaml:"ClearDays" default:"7"` +} + +type NodeDBConfig struct { + NodeSecret string + NodePullInterval int64 + NodePushInterval int64 + TrafficReportThreshold int64 + IPStrategy string + DNS string + Block string + Outbound string +} diff --git a/internal/handler/admin/application/createSubscribeApplicationHandler.go b/internal/handler/admin/application/createSubscribeApplicationHandler.go new file mode 100644 index 0000000..6f9d136 --- /dev/null +++ b/internal/handler/admin/application/createSubscribeApplicationHandler.go @@ -0,0 +1,26 @@ +package application + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/application" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Create subscribe application +func CreateSubscribeApplicationHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.CreateSubscribeApplicationRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := application.NewCreateSubscribeApplicationLogic(c.Request.Context(), svcCtx) + resp, err := l.CreateSubscribeApplication(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/server/batchDeleteNodeGroupHandler.go b/internal/handler/admin/application/deleteSubscribeApplicationHandler.go similarity index 50% rename from internal/handler/admin/server/batchDeleteNodeGroupHandler.go rename to internal/handler/admin/application/deleteSubscribeApplicationHandler.go index 22d0062..6e298a5 100644 --- a/internal/handler/admin/server/batchDeleteNodeGroupHandler.go +++ b/internal/handler/admin/application/deleteSubscribeApplicationHandler.go @@ -1,17 +1,17 @@ -package server +package application import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/admin/server" + "github.com/perfect-panel/server/internal/logic/admin/application" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/result" ) -// Batch delete node group -func BatchDeleteNodeGroupHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { +// Delete subscribe application +func DeleteSubscribeApplicationHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - var req types.BatchDeleteNodeGroupRequest + var req types.DeleteSubscribeApplicationRequest _ = c.ShouldBind(&req) validateErr := svcCtx.Validate(&req) if validateErr != nil { @@ -19,8 +19,8 @@ func BatchDeleteNodeGroupHandler(svcCtx *svc.ServiceContext) func(c *gin.Context return } - l := server.NewBatchDeleteNodeGroupLogic(c.Request.Context(), svcCtx) - err := l.BatchDeleteNodeGroup(&req) + l := application.NewDeleteSubscribeApplicationLogic(c.Request.Context(), svcCtx) + err := l.DeleteSubscribeApplication(&req) result.HttpResult(c, nil, err) } } diff --git a/internal/handler/admin/application/getSubscribeApplicationListHandler.go b/internal/handler/admin/application/getSubscribeApplicationListHandler.go new file mode 100644 index 0000000..5e8222d --- /dev/null +++ b/internal/handler/admin/application/getSubscribeApplicationListHandler.go @@ -0,0 +1,26 @@ +package application + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/application" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Get subscribe application list +func GetSubscribeApplicationListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetSubscribeApplicationListRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := application.NewGetSubscribeApplicationListLogic(c.Request.Context(), svcCtx) + resp, err := l.GetSubscribeApplicationList(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/app/subscribe/resetUserSubscribePeriodHandler.go b/internal/handler/admin/application/previewSubscribeTemplateHandler.go similarity index 54% rename from internal/handler/app/subscribe/resetUserSubscribePeriodHandler.go rename to internal/handler/admin/application/previewSubscribeTemplateHandler.go index fc939b6..6c136d2 100644 --- a/internal/handler/app/subscribe/resetUserSubscribePeriodHandler.go +++ b/internal/handler/admin/application/previewSubscribeTemplateHandler.go @@ -1,17 +1,17 @@ -package subscribe +package application import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/app/subscribe" + "github.com/perfect-panel/server/internal/logic/admin/application" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/result" ) -// Reset user subscription period -func ResetUserSubscribePeriodHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { +// Preview Template +func PreviewSubscribeTemplateHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - var req types.UserSubscribeResetPeriodRequest + var req types.PreviewSubscribeTemplateRequest _ = c.ShouldBind(&req) validateErr := svcCtx.Validate(&req) if validateErr != nil { @@ -19,8 +19,9 @@ func ResetUserSubscribePeriodHandler(svcCtx *svc.ServiceContext) func(c *gin.Con return } - l := subscribe.NewResetUserSubscribePeriodLogic(c.Request.Context(), svcCtx) - resp, err := l.ResetUserSubscribePeriod(&req) + l := application.NewPreviewSubscribeTemplateLogic(c.Request.Context(), svcCtx) + resp, err := l.PreviewSubscribeTemplate(&req) result.HttpResult(c, resp, err) + } } diff --git a/internal/handler/admin/application/updateSubscribeApplicationHandler.go b/internal/handler/admin/application/updateSubscribeApplicationHandler.go new file mode 100644 index 0000000..beaf306 --- /dev/null +++ b/internal/handler/admin/application/updateSubscribeApplicationHandler.go @@ -0,0 +1,26 @@ +package application + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/application" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Update subscribe application +func UpdateSubscribeApplicationHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UpdateSubscribeApplicationRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := application.NewUpdateSubscribeApplicationLogic(c.Request.Context(), svcCtx) + resp, err := l.UpdateSubscribeApplication(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/app/order/queryorderdetailhandler.go b/internal/handler/admin/log/filterBalanceLogHandler.go similarity index 58% rename from internal/handler/app/order/queryorderdetailhandler.go rename to internal/handler/admin/log/filterBalanceLogHandler.go index bb8a109..c8bf7d1 100644 --- a/internal/handler/app/order/queryorderdetailhandler.go +++ b/internal/handler/admin/log/filterBalanceLogHandler.go @@ -1,17 +1,17 @@ -package order +package log import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/app/order" + "github.com/perfect-panel/server/internal/logic/admin/log" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/result" ) -// Get order -func QueryOrderDetailHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { +// Filter balance log +func FilterBalanceLogHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - var req types.QueryOrderDetailRequest + var req types.FilterBalanceLogRequest _ = c.ShouldBind(&req) validateErr := svcCtx.Validate(&req) if validateErr != nil { @@ -19,8 +19,8 @@ func QueryOrderDetailHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return } - l := order.NewQueryOrderDetailLogic(c.Request.Context(), svcCtx) - resp, err := l.QueryOrderDetail(&req) + l := log.NewFilterBalanceLogLogic(c.Request.Context(), svcCtx) + resp, err := l.FilterBalanceLog(&req) result.HttpResult(c, resp, err) } } diff --git a/internal/handler/app/document/querydocumentdetailhandler.go b/internal/handler/admin/log/filterCommissionLogHandler.go similarity index 56% rename from internal/handler/app/document/querydocumentdetailhandler.go rename to internal/handler/admin/log/filterCommissionLogHandler.go index 7b0063d..07361cd 100644 --- a/internal/handler/app/document/querydocumentdetailhandler.go +++ b/internal/handler/admin/log/filterCommissionLogHandler.go @@ -1,17 +1,17 @@ -package document +package log import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/app/document" + "github.com/perfect-panel/server/internal/logic/admin/log" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/result" ) -// Get document detail -func QueryDocumentDetailHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { +// Filter commission log +func FilterCommissionLogHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - var req types.QueryDocumentDetailRequest + var req types.FilterCommissionLogRequest _ = c.ShouldBind(&req) validateErr := svcCtx.Validate(&req) if validateErr != nil { @@ -19,8 +19,8 @@ func QueryDocumentDetailHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) return } - l := document.NewQueryDocumentDetailLogic(c.Request.Context(), svcCtx) - resp, err := l.QueryDocumentDetail(&req) + l := log.NewFilterCommissionLogLogic(c.Request.Context(), svcCtx) + resp, err := l.FilterCommissionLog(&req) result.HttpResult(c, resp, err) } } diff --git a/internal/handler/app/order/queryorderlisthandler.go b/internal/handler/admin/log/filterEmailLogHandler.go similarity index 58% rename from internal/handler/app/order/queryorderlisthandler.go rename to internal/handler/admin/log/filterEmailLogHandler.go index 98e8575..6d9f03d 100644 --- a/internal/handler/app/order/queryorderlisthandler.go +++ b/internal/handler/admin/log/filterEmailLogHandler.go @@ -1,17 +1,17 @@ -package order +package log import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/app/order" + "github.com/perfect-panel/server/internal/logic/admin/log" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/result" ) -// Get order list -func QueryOrderListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { +// Filter email log +func FilterEmailLogHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - var req types.QueryOrderListRequest + var req types.FilterLogParams _ = c.ShouldBind(&req) validateErr := svcCtx.Validate(&req) if validateErr != nil { @@ -19,8 +19,8 @@ func QueryOrderListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return } - l := order.NewQueryOrderListLogic(c.Request.Context(), svcCtx) - resp, err := l.QueryOrderList(&req) + l := log.NewFilterEmailLogLogic(c.Request.Context(), svcCtx) + resp, err := l.FilterEmailLog(&req) result.HttpResult(c, resp, err) } } diff --git a/internal/handler/app/order/checkoutorderhandler.go b/internal/handler/admin/log/filterGiftLogHandler.go similarity index 57% rename from internal/handler/app/order/checkoutorderhandler.go rename to internal/handler/admin/log/filterGiftLogHandler.go index 6b16b39..e650a27 100644 --- a/internal/handler/app/order/checkoutorderhandler.go +++ b/internal/handler/admin/log/filterGiftLogHandler.go @@ -1,17 +1,17 @@ -package order +package log import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/app/order" + "github.com/perfect-panel/server/internal/logic/admin/log" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/result" ) -// Checkout order -func CheckoutOrderHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { +// Filter gift log +func FilterGiftLogHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - var req types.CheckoutOrderRequest + var req types.FilterGiftLogRequest _ = c.ShouldBind(&req) validateErr := svcCtx.Validate(&req) if validateErr != nil { @@ -19,8 +19,8 @@ func CheckoutOrderHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return } - l := order.NewCheckoutOrderLogic(c.Request.Context(), svcCtx) - resp, err := l.CheckoutOrder(&req, c.Request.Host) + l := log.NewFilterGiftLogLogic(c.Request.Context(), svcCtx) + resp, err := l.FilterGiftLog(&req) result.HttpResult(c, resp, err) } } diff --git a/internal/handler/app/order/precreateorderhandler.go b/internal/handler/admin/log/filterLoginLogHandler.go similarity index 58% rename from internal/handler/app/order/precreateorderhandler.go rename to internal/handler/admin/log/filterLoginLogHandler.go index 53b19a1..c2dae41 100644 --- a/internal/handler/app/order/precreateorderhandler.go +++ b/internal/handler/admin/log/filterLoginLogHandler.go @@ -1,17 +1,17 @@ -package order +package log import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/app/order" + "github.com/perfect-panel/server/internal/logic/admin/log" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/result" ) -// Pre create order -func PreCreateOrderHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { +// Filter login log +func FilterLoginLogHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - var req types.PurchaseOrderRequest + var req types.FilterLoginLogRequest _ = c.ShouldBind(&req) validateErr := svcCtx.Validate(&req) if validateErr != nil { @@ -19,8 +19,8 @@ func PreCreateOrderHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return } - l := order.NewPreCreateOrderLogic(c.Request.Context(), svcCtx) - resp, err := l.PreCreateOrder(&req) + l := log.NewFilterLoginLogLogic(c.Request.Context(), svcCtx) + resp, err := l.FilterLoginLog(&req) result.HttpResult(c, resp, err) } } diff --git a/internal/handler/app/node/getNodeListHandler.go b/internal/handler/admin/log/filterMobileLogHandler.go similarity index 56% rename from internal/handler/app/node/getNodeListHandler.go rename to internal/handler/admin/log/filterMobileLogHandler.go index 4363e35..0d45e27 100644 --- a/internal/handler/app/node/getNodeListHandler.go +++ b/internal/handler/admin/log/filterMobileLogHandler.go @@ -1,17 +1,17 @@ -package node +package log import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/app/node" + "github.com/perfect-panel/server/internal/logic/admin/log" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/result" ) -// Get Node list -func GetNodeListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { +// Filter mobile log +func FilterMobileLogHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - var req types.AppUserSubscbribeNodeRequest + var req types.FilterLogParams _ = c.ShouldBind(&req) validateErr := svcCtx.Validate(&req) if validateErr != nil { @@ -19,8 +19,8 @@ func GetNodeListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return } - l := node.NewGetNodeListLogic(c.Request.Context(), svcCtx) - resp, err := l.GetNodeList(&req) + l := log.NewFilterMobileLogLogic(c.Request.Context(), svcCtx) + resp, err := l.FilterMobileLog(&req) result.HttpResult(c, resp, err) } } diff --git a/internal/handler/app/announcement/queryannouncementhandler.go b/internal/handler/admin/log/filterRegisterLogHandler.go similarity index 55% rename from internal/handler/app/announcement/queryannouncementhandler.go rename to internal/handler/admin/log/filterRegisterLogHandler.go index 327ab1e..a5ca9e8 100644 --- a/internal/handler/app/announcement/queryannouncementhandler.go +++ b/internal/handler/admin/log/filterRegisterLogHandler.go @@ -1,17 +1,17 @@ -package announcement +package log import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/app/announcement" + "github.com/perfect-panel/server/internal/logic/admin/log" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/result" ) -// Query announcement -func QueryAnnouncementHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { +// Filter register log +func FilterRegisterLogHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - var req types.QueryAnnouncementRequest + var req types.FilterRegisterLogRequest _ = c.ShouldBind(&req) validateErr := svcCtx.Validate(&req) if validateErr != nil { @@ -19,8 +19,8 @@ func QueryAnnouncementHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return } - l := announcement.NewQueryAnnouncementLogic(c.Request.Context(), svcCtx) - resp, err := l.QueryAnnouncement(&req) + l := log.NewFilterRegisterLogLogic(c.Request.Context(), svcCtx) + resp, err := l.FilterRegisterLog(&req) result.HttpResult(c, resp, err) } } diff --git a/internal/handler/admin/log/filterResetSubscribeLogHandler.go b/internal/handler/admin/log/filterResetSubscribeLogHandler.go new file mode 100644 index 0000000..f4d96e5 --- /dev/null +++ b/internal/handler/admin/log/filterResetSubscribeLogHandler.go @@ -0,0 +1,26 @@ +package log + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/log" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Filter reset subscribe log +func FilterResetSubscribeLogHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.FilterResetSubscribeLogRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := log.NewFilterResetSubscribeLogLogic(c.Request.Context(), svcCtx) + resp, err := l.FilterResetSubscribeLog(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/app/user/queryuseraffiliatelisthandler.go b/internal/handler/admin/log/filterServerTrafficLogHandler.go similarity index 56% rename from internal/handler/app/user/queryuseraffiliatelisthandler.go rename to internal/handler/admin/log/filterServerTrafficLogHandler.go index 3368cbd..ec522ed 100644 --- a/internal/handler/app/user/queryuseraffiliatelisthandler.go +++ b/internal/handler/admin/log/filterServerTrafficLogHandler.go @@ -1,17 +1,17 @@ -package user +package log import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/app/user" + "github.com/perfect-panel/server/internal/logic/admin/log" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/result" ) -// Query User Affiliate List -func QueryUserAffiliateListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { +// Filter server traffic log +func FilterServerTrafficLogHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - var req types.QueryUserAffiliateListRequest + var req types.FilterServerTrafficLogRequest _ = c.ShouldBind(&req) validateErr := svcCtx.Validate(&req) if validateErr != nil { @@ -19,8 +19,8 @@ func QueryUserAffiliateListHandler(svcCtx *svc.ServiceContext) func(c *gin.Conte return } - l := user.NewQueryUserAffiliateListLogic(c.Request.Context(), svcCtx) - resp, err := l.QueryUserAffiliateList(&req) + l := log.NewFilterServerTrafficLogLogic(c.Request.Context(), svcCtx) + resp, err := l.FilterServerTrafficLog(&req) result.HttpResult(c, resp, err) } } diff --git a/internal/handler/app/auth/getAppConfigHandler.go b/internal/handler/admin/log/filterSubscribeLogHandler.go similarity index 55% rename from internal/handler/app/auth/getAppConfigHandler.go rename to internal/handler/admin/log/filterSubscribeLogHandler.go index 088525b..f01b61e 100644 --- a/internal/handler/app/auth/getAppConfigHandler.go +++ b/internal/handler/admin/log/filterSubscribeLogHandler.go @@ -1,17 +1,17 @@ -package auth +package log import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/app/auth" + "github.com/perfect-panel/server/internal/logic/admin/log" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/result" ) -// GetAppConfig -func GetAppConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { +// Filter subscribe log +func FilterSubscribeLogHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - var req types.AppConfigRequest + var req types.FilterSubscribeLogRequest _ = c.ShouldBind(&req) validateErr := svcCtx.Validate(&req) if validateErr != nil { @@ -19,8 +19,8 @@ func GetAppConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return } - l := auth.NewGetAppConfigLogic(c, svcCtx) - resp, err := l.GetAppConfig(&req) + l := log.NewFilterSubscribeLogLogic(c.Request.Context(), svcCtx) + resp, err := l.FilterSubscribeLog(&req) result.HttpResult(c, resp, err) } } diff --git a/internal/handler/admin/log/filterTrafficLogDetailsHandler.go b/internal/handler/admin/log/filterTrafficLogDetailsHandler.go new file mode 100644 index 0000000..c77a881 --- /dev/null +++ b/internal/handler/admin/log/filterTrafficLogDetailsHandler.go @@ -0,0 +1,26 @@ +package log + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/log" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Filter traffic log details +func FilterTrafficLogDetailsHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.FilterTrafficLogDetailsRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := log.NewFilterTrafficLogDetailsLogic(c.Request.Context(), svcCtx) + resp, err := l.FilterTrafficLogDetails(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/log/filterUserSubscribeTrafficLogHandler.go b/internal/handler/admin/log/filterUserSubscribeTrafficLogHandler.go new file mode 100644 index 0000000..976e278 --- /dev/null +++ b/internal/handler/admin/log/filterUserSubscribeTrafficLogHandler.go @@ -0,0 +1,26 @@ +package log + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/log" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Filter user subscribe traffic log +func FilterUserSubscribeTrafficLogHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.FilterSubscribeTrafficRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := log.NewFilterUserSubscribeTrafficLogLogic(c.Request.Context(), svcCtx) + resp, err := l.FilterUserSubscribeTrafficLog(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/log/getLogSettingHandler.go b/internal/handler/admin/log/getLogSettingHandler.go new file mode 100644 index 0000000..50217cb --- /dev/null +++ b/internal/handler/admin/log/getLogSettingHandler.go @@ -0,0 +1,18 @@ +package log + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/log" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" +) + +// Get log setting +func GetLogSettingHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := log.NewGetLogSettingLogic(c.Request.Context(), svcCtx) + resp, err := l.GetLogSetting() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/app/order/closeorderhandler.go b/internal/handler/admin/log/updateLogSettingHandler.go similarity index 57% rename from internal/handler/app/order/closeorderhandler.go rename to internal/handler/admin/log/updateLogSettingHandler.go index bba2210..91aa2c8 100644 --- a/internal/handler/app/order/closeorderhandler.go +++ b/internal/handler/admin/log/updateLogSettingHandler.go @@ -1,17 +1,17 @@ -package order +package log import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/app/order" + "github.com/perfect-panel/server/internal/logic/admin/log" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/result" ) -// Close order -func CloseOrderHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { +// Update log setting +func UpdateLogSettingHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - var req types.CloseOrderRequest + var req types.LogSetting _ = c.ShouldBind(&req) validateErr := svcCtx.Validate(&req) if validateErr != nil { @@ -19,8 +19,8 @@ func CloseOrderHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return } - l := order.NewCloseOrderLogic(c.Request.Context(), svcCtx) - err := l.CloseOrder(&req) + l := log.NewUpdateLogSettingLogic(c.Request.Context(), svcCtx) + err := l.UpdateLogSetting(&req) result.HttpResult(c, nil, err) } } diff --git a/internal/handler/admin/system/createApplicationVersionHandler.go b/internal/handler/admin/marketing/createBatchSendEmailTaskHandler.go similarity index 54% rename from internal/handler/admin/system/createApplicationVersionHandler.go rename to internal/handler/admin/marketing/createBatchSendEmailTaskHandler.go index bc92fa2..8057689 100644 --- a/internal/handler/admin/system/createApplicationVersionHandler.go +++ b/internal/handler/admin/marketing/createBatchSendEmailTaskHandler.go @@ -1,17 +1,17 @@ -package system +package marketing import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/admin/system" + "github.com/perfect-panel/server/internal/logic/admin/marketing" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/result" ) -// Create application version -func CreateApplicationVersionHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { +// Create a batch send email task +func CreateBatchSendEmailTaskHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - var req types.CreateApplicationVersionRequest + var req types.CreateBatchSendEmailTaskRequest _ = c.ShouldBind(&req) validateErr := svcCtx.Validate(&req) if validateErr != nil { @@ -19,8 +19,8 @@ func CreateApplicationVersionHandler(svcCtx *svc.ServiceContext) func(c *gin.Con return } - l := system.NewCreateApplicationVersionLogic(c.Request.Context(), svcCtx) - err := l.CreateApplicationVersion(&req) + l := marketing.NewCreateBatchSendEmailTaskLogic(c.Request.Context(), svcCtx) + err := l.CreateBatchSendEmailTask(&req) result.HttpResult(c, nil, err) } } diff --git a/internal/handler/admin/marketing/createQuotaTaskHandler.go b/internal/handler/admin/marketing/createQuotaTaskHandler.go new file mode 100644 index 0000000..0fb088b --- /dev/null +++ b/internal/handler/admin/marketing/createQuotaTaskHandler.go @@ -0,0 +1,26 @@ +package marketing + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/marketing" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Create a quota task +func CreateQuotaTaskHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.CreateQuotaTaskRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := marketing.NewCreateQuotaTaskLogic(c.Request.Context(), svcCtx) + err := l.CreateQuotaTask(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/marketing/getBatchSendEmailTaskListHandler.go b/internal/handler/admin/marketing/getBatchSendEmailTaskListHandler.go new file mode 100644 index 0000000..9c13f9f --- /dev/null +++ b/internal/handler/admin/marketing/getBatchSendEmailTaskListHandler.go @@ -0,0 +1,26 @@ +package marketing + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/marketing" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Get batch send email task list +func GetBatchSendEmailTaskListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetBatchSendEmailTaskListRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := marketing.NewGetBatchSendEmailTaskListLogic(c.Request.Context(), svcCtx) + resp, err := l.GetBatchSendEmailTaskList(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/app/user/getusersubscribetrafficlogshandler.go b/internal/handler/admin/marketing/getBatchSendEmailTaskStatusHandler.go similarity index 50% rename from internal/handler/app/user/getusersubscribetrafficlogshandler.go rename to internal/handler/admin/marketing/getBatchSendEmailTaskStatusHandler.go index 977fbb9..8e5ad87 100644 --- a/internal/handler/app/user/getusersubscribetrafficlogshandler.go +++ b/internal/handler/admin/marketing/getBatchSendEmailTaskStatusHandler.go @@ -1,26 +1,26 @@ -package user +package marketing import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/app/user" + "github.com/perfect-panel/server/internal/logic/admin/marketing" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/result" ) -// Get user subcribe traffic logs -func GetUserSubscribeTrafficLogsHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { +// Get batch send email task status +func GetBatchSendEmailTaskStatusHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - var req types.GetUserSubscribeTrafficLogsRequest - _ = c.BindQuery(&req) + var req types.GetBatchSendEmailTaskStatusRequest + _ = c.ShouldBind(&req) validateErr := svcCtx.Validate(&req) if validateErr != nil { result.ParamErrorResult(c, validateErr) return } - l := user.NewGetUserSubscribeTrafficLogsLogic(c.Request.Context(), svcCtx) - resp, err := l.GetUserSubscribeTrafficLogs(&req) + l := marketing.NewGetBatchSendEmailTaskStatusLogic(c.Request.Context(), svcCtx) + resp, err := l.GetBatchSendEmailTaskStatus(&req) result.HttpResult(c, resp, err) } } diff --git a/internal/handler/admin/marketing/getPreSendEmailCountHandler.go b/internal/handler/admin/marketing/getPreSendEmailCountHandler.go new file mode 100644 index 0000000..33e5894 --- /dev/null +++ b/internal/handler/admin/marketing/getPreSendEmailCountHandler.go @@ -0,0 +1,26 @@ +package marketing + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/marketing" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Get pre-send email count +func GetPreSendEmailCountHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetPreSendEmailCountRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := marketing.NewGetPreSendEmailCountLogic(c.Request.Context(), svcCtx) + resp, err := l.GetPreSendEmailCount(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/marketing/queryQuotaTaskListHandler.go b/internal/handler/admin/marketing/queryQuotaTaskListHandler.go new file mode 100644 index 0000000..3aaebdc --- /dev/null +++ b/internal/handler/admin/marketing/queryQuotaTaskListHandler.go @@ -0,0 +1,26 @@ +package marketing + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/marketing" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Query quota task list +func QueryQuotaTaskListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.QueryQuotaTaskListRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := marketing.NewQueryQuotaTaskListLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryQuotaTaskList(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/marketing/queryQuotaTaskPreCountHandler.go b/internal/handler/admin/marketing/queryQuotaTaskPreCountHandler.go new file mode 100644 index 0000000..bcf6bd7 --- /dev/null +++ b/internal/handler/admin/marketing/queryQuotaTaskPreCountHandler.go @@ -0,0 +1,26 @@ +package marketing + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/marketing" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Query quota task pre-count +func QueryQuotaTaskPreCountHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.QueryQuotaTaskPreCountRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := marketing.NewQueryQuotaTaskPreCountLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryQuotaTaskPreCount(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/marketing/queryQuotaTaskStatusHandler.go b/internal/handler/admin/marketing/queryQuotaTaskStatusHandler.go new file mode 100644 index 0000000..8d6cf9c --- /dev/null +++ b/internal/handler/admin/marketing/queryQuotaTaskStatusHandler.go @@ -0,0 +1,26 @@ +package marketing + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/marketing" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Query quota task status +func QueryQuotaTaskStatusHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.QueryQuotaTaskStatusRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := marketing.NewQueryQuotaTaskStatusLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryQuotaTaskStatus(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/marketing/stopBatchSendEmailTaskHandler.go b/internal/handler/admin/marketing/stopBatchSendEmailTaskHandler.go new file mode 100644 index 0000000..0129218 --- /dev/null +++ b/internal/handler/admin/marketing/stopBatchSendEmailTaskHandler.go @@ -0,0 +1,26 @@ +package marketing + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/marketing" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// StopBatchSendEmailTaskHandler Stop a batch send email task +func StopBatchSendEmailTaskHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.StopBatchSendEmailTaskRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := marketing.NewStopBatchSendEmailTaskLogic(c.Request.Context(), svcCtx) + err := l.StopBatchSendEmailTask(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/server/createNodeHandler.go b/internal/handler/admin/server/createNodeHandler.go index cac4455..e872b09 100644 --- a/internal/handler/admin/server/createNodeHandler.go +++ b/internal/handler/admin/server/createNodeHandler.go @@ -8,7 +8,7 @@ import ( "github.com/perfect-panel/server/pkg/result" ) -// Create node +// Create Node func CreateNodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { var req types.CreateNodeRequest diff --git a/internal/handler/admin/server/createNodeGroupHandler.go b/internal/handler/admin/server/createServerHandler.go similarity index 66% rename from internal/handler/admin/server/createNodeGroupHandler.go rename to internal/handler/admin/server/createServerHandler.go index edbc8f9..2068122 100644 --- a/internal/handler/admin/server/createNodeGroupHandler.go +++ b/internal/handler/admin/server/createServerHandler.go @@ -8,10 +8,10 @@ import ( "github.com/perfect-panel/server/pkg/result" ) -// Create node group -func CreateNodeGroupHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { +// CreateServerHandler Create Server +func CreateServerHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - var req types.CreateNodeGroupRequest + var req types.CreateServerRequest _ = c.ShouldBind(&req) validateErr := svcCtx.Validate(&req) if validateErr != nil { @@ -19,8 +19,8 @@ func CreateNodeGroupHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return } - l := server.NewCreateNodeGroupLogic(c.Request.Context(), svcCtx) - err := l.CreateNodeGroup(&req) + l := server.NewCreateServerLogic(c.Request.Context(), svcCtx) + err := l.CreateServer(&req) result.HttpResult(c, nil, err) } } diff --git a/internal/handler/admin/server/deleteNodeHandler.go b/internal/handler/admin/server/deleteNodeHandler.go index 76d3d49..37ac80b 100644 --- a/internal/handler/admin/server/deleteNodeHandler.go +++ b/internal/handler/admin/server/deleteNodeHandler.go @@ -8,7 +8,7 @@ import ( "github.com/perfect-panel/server/pkg/result" ) -// Delete node +// Delete Node func DeleteNodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { var req types.DeleteNodeRequest diff --git a/internal/handler/admin/server/nodeSortHandler.go b/internal/handler/admin/server/deleteServerHandler.go similarity index 68% rename from internal/handler/admin/server/nodeSortHandler.go rename to internal/handler/admin/server/deleteServerHandler.go index 2b4f031..677fb17 100644 --- a/internal/handler/admin/server/nodeSortHandler.go +++ b/internal/handler/admin/server/deleteServerHandler.go @@ -8,10 +8,10 @@ import ( "github.com/perfect-panel/server/pkg/result" ) -// Node sort -func NodeSortHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { +// Delete Server +func DeleteServerHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - var req types.NodeSortRequest + var req types.DeleteServerRequest _ = c.ShouldBind(&req) validateErr := svcCtx.Validate(&req) if validateErr != nil { @@ -19,8 +19,8 @@ func NodeSortHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return } - l := server.NewNodeSortLogic(c.Request.Context(), svcCtx) - err := l.NodeSort(&req) + l := server.NewDeleteServerLogic(c.Request.Context(), svcCtx) + err := l.DeleteServer(&req) result.HttpResult(c, nil, err) } } diff --git a/internal/handler/admin/server/getNodeDetailHandler.go b/internal/handler/admin/server/filterNodeListHandler.go similarity index 66% rename from internal/handler/admin/server/getNodeDetailHandler.go rename to internal/handler/admin/server/filterNodeListHandler.go index 06bcaab..5e154ca 100644 --- a/internal/handler/admin/server/getNodeDetailHandler.go +++ b/internal/handler/admin/server/filterNodeListHandler.go @@ -8,10 +8,10 @@ import ( "github.com/perfect-panel/server/pkg/result" ) -// Get node detail -func GetNodeDetailHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { +// Filter Node List +func FilterNodeListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - var req types.GetDetailRequest + var req types.FilterNodeListRequest _ = c.ShouldBind(&req) validateErr := svcCtx.Validate(&req) if validateErr != nil { @@ -19,8 +19,8 @@ func GetNodeDetailHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return } - l := server.NewGetNodeDetailLogic(c.Request.Context(), svcCtx) - resp, err := l.GetNodeDetail(&req) + l := server.NewFilterNodeListLogic(c.Request.Context(), svcCtx) + resp, err := l.FilterNodeList(&req) result.HttpResult(c, resp, err) } } diff --git a/internal/handler/admin/server/createRuleGroupHandler.go b/internal/handler/admin/server/filterServerListHandler.go similarity index 59% rename from internal/handler/admin/server/createRuleGroupHandler.go rename to internal/handler/admin/server/filterServerListHandler.go index 89a436a..9e6cb7b 100644 --- a/internal/handler/admin/server/createRuleGroupHandler.go +++ b/internal/handler/admin/server/filterServerListHandler.go @@ -8,10 +8,10 @@ import ( "github.com/perfect-panel/server/pkg/result" ) -// CreateRuleGroupHandler Create rule group -func CreateRuleGroupHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { +// FilterServerListHandler Filter Server List +func FilterServerListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - var req types.CreateRuleGroupRequest + var req types.FilterServerListRequest _ = c.ShouldBind(&req) validateErr := svcCtx.Validate(&req) if validateErr != nil { @@ -19,8 +19,8 @@ func CreateRuleGroupHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return } - l := server.NewCreateRuleGroupLogic(c.Request.Context(), svcCtx) - err := l.CreateRuleGroup(&req) - result.HttpResult(c, nil, err) + l := server.NewFilterServerListLogic(c.Request.Context(), svcCtx) + resp, err := l.FilterServerList(&req) + result.HttpResult(c, resp, err) } } diff --git a/internal/handler/admin/server/getNodeListHandler.go b/internal/handler/admin/server/getServerProtocolsHandler.go similarity index 64% rename from internal/handler/admin/server/getNodeListHandler.go rename to internal/handler/admin/server/getServerProtocolsHandler.go index 3bfa07d..14238ca 100644 --- a/internal/handler/admin/server/getNodeListHandler.go +++ b/internal/handler/admin/server/getServerProtocolsHandler.go @@ -8,10 +8,10 @@ import ( "github.com/perfect-panel/server/pkg/result" ) -// Get node list -func GetNodeListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { +// Get Server Protocols +func GetServerProtocolsHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - var req types.GetNodeServerListRequest + var req types.GetServerProtocolsRequest _ = c.ShouldBind(&req) validateErr := svcCtx.Validate(&req) if validateErr != nil { @@ -19,8 +19,8 @@ func GetNodeListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return } - l := server.NewGetNodeListLogic(c.Request.Context(), svcCtx) - resp, err := l.GetNodeList(&req) + l := server.NewGetServerProtocolsLogic(c.Request.Context(), svcCtx) + resp, err := l.GetServerProtocols(&req) result.HttpResult(c, resp, err) } } diff --git a/internal/handler/admin/server/getNodeGroupListHandler.go b/internal/handler/admin/server/hasMigrateSeverNodeHandler.go similarity index 53% rename from internal/handler/admin/server/getNodeGroupListHandler.go rename to internal/handler/admin/server/hasMigrateSeverNodeHandler.go index 3d741d0..6088577 100644 --- a/internal/handler/admin/server/getNodeGroupListHandler.go +++ b/internal/handler/admin/server/hasMigrateSeverNodeHandler.go @@ -7,12 +7,12 @@ import ( "github.com/perfect-panel/server/pkg/result" ) -// Get node group list -func GetNodeGroupListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { +// Check if there is any server or node to migrate +func HasMigrateSeverNodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - l := server.NewGetNodeGroupListLogic(c.Request.Context(), svcCtx) - resp, err := l.GetNodeGroupList() + l := server.NewHasMigrateSeverNodeLogic(c.Request.Context(), svcCtx) + resp, err := l.HasMigrateSeverNode() result.HttpResult(c, resp, err) } } diff --git a/internal/handler/admin/server/getRuleGroupListHandler.go b/internal/handler/admin/server/migrateServerNodeHandler.go similarity index 54% rename from internal/handler/admin/server/getRuleGroupListHandler.go rename to internal/handler/admin/server/migrateServerNodeHandler.go index 8f8fba2..8f8c842 100644 --- a/internal/handler/admin/server/getRuleGroupListHandler.go +++ b/internal/handler/admin/server/migrateServerNodeHandler.go @@ -7,12 +7,12 @@ import ( "github.com/perfect-panel/server/pkg/result" ) -// Get rule group list -func GetRuleGroupListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { +// Migrate server and node data to new database +func MigrateServerNodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - l := server.NewGetRuleGroupListLogic(c.Request.Context(), svcCtx) - resp, err := l.GetRuleGroupList() + l := server.NewMigrateServerNodeLogic(c.Request.Context(), svcCtx) + resp, err := l.MigrateServerNode() result.HttpResult(c, resp, err) } } diff --git a/internal/handler/admin/server/getNodeTagListHandler.go b/internal/handler/admin/server/queryNodeTagHandler.go similarity index 58% rename from internal/handler/admin/server/getNodeTagListHandler.go rename to internal/handler/admin/server/queryNodeTagHandler.go index aa0e1ff..fa963cc 100644 --- a/internal/handler/admin/server/getNodeTagListHandler.go +++ b/internal/handler/admin/server/queryNodeTagHandler.go @@ -7,12 +7,12 @@ import ( "github.com/perfect-panel/server/pkg/result" ) -// Get node tag list -func GetNodeTagListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { +// Query all node tags +func QueryNodeTagHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - l := server.NewGetNodeTagListLogic(c.Request.Context(), svcCtx) - resp, err := l.GetNodeTagList() + l := server.NewQueryNodeTagLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryNodeTag() result.HttpResult(c, resp, err) } } diff --git a/internal/handler/admin/server/deleteNodeGroupHandler.go b/internal/handler/admin/server/resetSortWithNodeHandler.go similarity index 66% rename from internal/handler/admin/server/deleteNodeGroupHandler.go rename to internal/handler/admin/server/resetSortWithNodeHandler.go index d27c6b9..4b8b14c 100644 --- a/internal/handler/admin/server/deleteNodeGroupHandler.go +++ b/internal/handler/admin/server/resetSortWithNodeHandler.go @@ -8,10 +8,10 @@ import ( "github.com/perfect-panel/server/pkg/result" ) -// Delete node group -func DeleteNodeGroupHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { +// Reset node sort +func ResetSortWithNodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - var req types.DeleteNodeGroupRequest + var req types.ResetSortRequest _ = c.ShouldBind(&req) validateErr := svcCtx.Validate(&req) if validateErr != nil { @@ -19,8 +19,8 @@ func DeleteNodeGroupHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return } - l := server.NewDeleteNodeGroupLogic(c.Request.Context(), svcCtx) - err := l.DeleteNodeGroup(&req) + l := server.NewResetSortWithNodeLogic(c.Request.Context(), svcCtx) + err := l.ResetSortWithNode(&req) result.HttpResult(c, nil, err) } } diff --git a/internal/handler/admin/server/resetSortWithServerHandler.go b/internal/handler/admin/server/resetSortWithServerHandler.go new file mode 100644 index 0000000..7adbecb --- /dev/null +++ b/internal/handler/admin/server/resetSortWithServerHandler.go @@ -0,0 +1,26 @@ +package server + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/server" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Reset server sort +func ResetSortWithServerHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.ResetSortRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := server.NewResetSortWithServerLogic(c.Request.Context(), svcCtx) + err := l.ResetSortWithServer(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/server/batchDeleteNodeHandler.go b/internal/handler/admin/server/toggleNodeStatusHandler.go similarity index 66% rename from internal/handler/admin/server/batchDeleteNodeHandler.go rename to internal/handler/admin/server/toggleNodeStatusHandler.go index ab5e3c3..67144ad 100644 --- a/internal/handler/admin/server/batchDeleteNodeHandler.go +++ b/internal/handler/admin/server/toggleNodeStatusHandler.go @@ -8,10 +8,10 @@ import ( "github.com/perfect-panel/server/pkg/result" ) -// Batch delete node -func BatchDeleteNodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { +// Toggle Node Status +func ToggleNodeStatusHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - var req types.BatchDeleteNodeRequest + var req types.ToggleNodeStatusRequest _ = c.ShouldBind(&req) validateErr := svcCtx.Validate(&req) if validateErr != nil { @@ -19,8 +19,8 @@ func BatchDeleteNodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return } - l := server.NewBatchDeleteNodeLogic(c.Request.Context(), svcCtx) - err := l.BatchDeleteNode(&req) + l := server.NewToggleNodeStatusLogic(c.Request.Context(), svcCtx) + err := l.ToggleNodeStatus(&req) result.HttpResult(c, nil, err) } } diff --git a/internal/handler/admin/server/updateNodeGroupHandler.go b/internal/handler/admin/server/updateNodeGroupHandler.go deleted file mode 100644 index 9994bf7..0000000 --- a/internal/handler/admin/server/updateNodeGroupHandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package server - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/admin/server" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/result" -) - -// Update node group -func UpdateNodeGroupHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.UpdateNodeGroupRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := server.NewUpdateNodeGroupLogic(c.Request.Context(), svcCtx) - err := l.UpdateNodeGroup(&req) - result.HttpResult(c, nil, err) - } -} diff --git a/internal/handler/admin/server/updateNodeHandler.go b/internal/handler/admin/server/updateNodeHandler.go index 58e8690..af19537 100644 --- a/internal/handler/admin/server/updateNodeHandler.go +++ b/internal/handler/admin/server/updateNodeHandler.go @@ -8,7 +8,7 @@ import ( "github.com/perfect-panel/server/pkg/result" ) -// Update node +// Update Node func UpdateNodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { var req types.UpdateNodeRequest diff --git a/internal/handler/admin/server/updateRuleGroupHandler.go b/internal/handler/admin/server/updateRuleGroupHandler.go deleted file mode 100644 index 224cb35..0000000 --- a/internal/handler/admin/server/updateRuleGroupHandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package server - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/admin/server" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/result" -) - -// Update rule group -func UpdateRuleGroupHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.UpdateRuleGroupRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := server.NewUpdateRuleGroupLogic(c.Request.Context(), svcCtx) - err := l.UpdateRuleGroup(&req) - result.HttpResult(c, nil, err) - } -} diff --git a/internal/handler/admin/server/deleteRuleGroupHandler.go b/internal/handler/admin/server/updateServerHandler.go similarity index 66% rename from internal/handler/admin/server/deleteRuleGroupHandler.go rename to internal/handler/admin/server/updateServerHandler.go index 8daddfd..24570c6 100644 --- a/internal/handler/admin/server/deleteRuleGroupHandler.go +++ b/internal/handler/admin/server/updateServerHandler.go @@ -8,10 +8,10 @@ import ( "github.com/perfect-panel/server/pkg/result" ) -// Delete rule group -func DeleteRuleGroupHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { +// Update Server +func UpdateServerHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - var req types.DeleteRuleGroupRequest + var req types.UpdateServerRequest _ = c.ShouldBind(&req) validateErr := svcCtx.Validate(&req) if validateErr != nil { @@ -19,8 +19,8 @@ func DeleteRuleGroupHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return } - l := server.NewDeleteRuleGroupLogic(c.Request.Context(), svcCtx) - err := l.DeleteRuleGroup(&req) + l := server.NewUpdateServerLogic(c.Request.Context(), svcCtx) + err := l.UpdateServer(&req) result.HttpResult(c, nil, err) } } diff --git a/internal/handler/admin/subscribe/resetAllSubscribeTokenHandler.go b/internal/handler/admin/subscribe/resetAllSubscribeTokenHandler.go new file mode 100644 index 0000000..408975a --- /dev/null +++ b/internal/handler/admin/subscribe/resetAllSubscribeTokenHandler.go @@ -0,0 +1,18 @@ +package subscribe + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/subscribe" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" +) + +// Reset all subscribe tokens +func ResetAllSubscribeTokenHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := subscribe.NewResetAllSubscribeTokenLogic(c.Request.Context(), svcCtx) + resp, err := l.ResetAllSubscribeToken() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/system/createApplicationHandler.go b/internal/handler/admin/system/createApplicationHandler.go deleted file mode 100644 index d429250..0000000 --- a/internal/handler/admin/system/createApplicationHandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package system - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/admin/system" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/result" -) - -// Create application -func CreateApplicationHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.CreateApplicationRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := system.NewCreateApplicationLogic(c.Request.Context(), svcCtx) - err := l.CreateApplication(&req) - result.HttpResult(c, nil, err) - } -} diff --git a/internal/handler/admin/system/deleteApplicationHandler.go b/internal/handler/admin/system/deleteApplicationHandler.go deleted file mode 100644 index a015e27..0000000 --- a/internal/handler/admin/system/deleteApplicationHandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package system - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/admin/system" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/result" -) - -// Delete application -func DeleteApplicationHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.DeleteApplicationRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := system.NewDeleteApplicationLogic(c.Request.Context(), svcCtx) - err := l.DeleteApplication(&req) - result.HttpResult(c, nil, err) - } -} diff --git a/internal/handler/admin/system/deleteApplicationVersionHandler.go b/internal/handler/admin/system/deleteApplicationVersionHandler.go deleted file mode 100644 index 2bb8109..0000000 --- a/internal/handler/admin/system/deleteApplicationVersionHandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package system - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/admin/system" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/result" -) - -// Delete application -func DeleteApplicationVersionHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.DeleteApplicationVersionRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := system.NewDeleteApplicationVersionLogic(c.Request.Context(), svcCtx) - err := l.DeleteApplicationVersion(&req) - result.HttpResult(c, nil, err) - } -} diff --git a/internal/handler/admin/system/getApplicationConfigHandler.go b/internal/handler/admin/system/getApplicationConfigHandler.go deleted file mode 100644 index 5d9cf9a..0000000 --- a/internal/handler/admin/system/getApplicationConfigHandler.go +++ /dev/null @@ -1,18 +0,0 @@ -package system - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/admin/system" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/pkg/result" -) - -// get application config -func GetApplicationConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - - l := system.NewGetApplicationConfigLogic(c.Request.Context(), svcCtx) - resp, err := l.GetApplicationConfig() - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/admin/system/getApplicationHandler.go b/internal/handler/admin/system/getModuleConfigHandler.go similarity index 55% rename from internal/handler/admin/system/getApplicationHandler.go rename to internal/handler/admin/system/getModuleConfigHandler.go index 0332928..72f87a3 100644 --- a/internal/handler/admin/system/getApplicationHandler.go +++ b/internal/handler/admin/system/getModuleConfigHandler.go @@ -7,12 +7,12 @@ import ( "github.com/perfect-panel/server/pkg/result" ) -// Get application -func GetApplicationHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { +// GetModuleConfigHandler Get Module Config +func GetModuleConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - l := system.NewGetApplicationLogic(c.Request.Context(), svcCtx) - resp, err := l.GetApplication() + l := system.NewGetModuleConfigLogic(c.Request.Context(), svcCtx) + resp, err := l.GetModuleConfig() result.HttpResult(c, resp, err) } } diff --git a/internal/handler/admin/system/getSubscribeTypeHandler.go b/internal/handler/admin/system/preViewNodeMultiplierHandler.go similarity index 55% rename from internal/handler/admin/system/getSubscribeTypeHandler.go rename to internal/handler/admin/system/preViewNodeMultiplierHandler.go index 7aae564..8ef2089 100644 --- a/internal/handler/admin/system/getSubscribeTypeHandler.go +++ b/internal/handler/admin/system/preViewNodeMultiplierHandler.go @@ -7,12 +7,12 @@ import ( "github.com/perfect-panel/server/pkg/result" ) -// Get subscribe type -func GetSubscribeTypeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { +// PreView Node Multiplier +func PreViewNodeMultiplierHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - l := system.NewGetSubscribeTypeLogic(c.Request.Context(), svcCtx) - resp, err := l.GetSubscribeType() + l := system.NewPreViewNodeMultiplierLogic(c.Request.Context(), svcCtx) + resp, err := l.PreViewNodeMultiplier() result.HttpResult(c, resp, err) } } diff --git a/internal/handler/admin/system/updateApplicationHandler.go b/internal/handler/admin/system/updateApplicationHandler.go deleted file mode 100644 index 55c0498..0000000 --- a/internal/handler/admin/system/updateApplicationHandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package system - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/admin/system" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/result" -) - -// Update application -func UpdateApplicationHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.UpdateApplicationRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := system.NewUpdateApplicationLogic(c.Request.Context(), svcCtx) - err := l.UpdateApplication(&req) - result.HttpResult(c, nil, err) - } -} diff --git a/internal/handler/admin/system/updateApplicationVersionHandler.go b/internal/handler/admin/system/updateApplicationVersionHandler.go deleted file mode 100644 index 1975338..0000000 --- a/internal/handler/admin/system/updateApplicationVersionHandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package system - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/admin/system" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/result" -) - -// Update application version -func UpdateApplicationVersionHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.UpdateApplicationVersionRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := system.NewUpdateApplicationVersionLogic(c.Request.Context(), svcCtx) - err := l.UpdateApplicationVersion(&req) - result.HttpResult(c, nil, err) - } -} diff --git a/internal/handler/admin/tool/getVersionHandler.go b/internal/handler/admin/tool/getVersionHandler.go new file mode 100644 index 0000000..d1c0bce --- /dev/null +++ b/internal/handler/admin/tool/getVersionHandler.go @@ -0,0 +1,18 @@ +package tool + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/tool" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" +) + +// GetVersionHandler Get Version +func GetVersionHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := tool.NewGetVersionLogic(c.Request.Context(), svcCtx) + resp, err := l.GetVersion() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/tool/queryIPLocationHandler.go b/internal/handler/admin/tool/queryIPLocationHandler.go new file mode 100644 index 0000000..0b95355 --- /dev/null +++ b/internal/handler/admin/tool/queryIPLocationHandler.go @@ -0,0 +1,26 @@ +package tool + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/tool" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// QueryIPLocationHandler Query IP Location +func QueryIPLocationHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.QueryIPLocationRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := tool.NewQueryIPLocationLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryIPLocation(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/user/getUserSubscribeResetTrafficLogsHandler.go b/internal/handler/admin/user/getUserSubscribeResetTrafficLogsHandler.go new file mode 100644 index 0000000..0f7525d --- /dev/null +++ b/internal/handler/admin/user/getUserSubscribeResetTrafficLogsHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Get user subcribe reset traffic logs +func GetUserSubscribeResetTrafficLogsHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetUserSubscribeResetTrafficLogsRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewGetUserSubscribeResetTrafficLogsLogic(c.Request.Context(), svcCtx) + resp, err := l.GetUserSubscribeResetTrafficLogs(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/app/auth/loginHandler.go b/internal/handler/app/auth/loginHandler.go deleted file mode 100644 index 4d6cd27..0000000 --- a/internal/handler/app/auth/loginHandler.go +++ /dev/null @@ -1,42 +0,0 @@ -package auth - -import ( - "time" - - "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/app/auth" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/result" - "github.com/perfect-panel/server/pkg/turnstile" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" -) - -// Login -func LoginHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.AppAuthRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - if svcCtx.Config.Verify.LoginVerify { - verifyTurns := turnstile.New(turnstile.Config{ - Secret: svcCtx.Config.Verify.TurnstileSecret, - Timeout: 3 * time.Second, - }) - if verify, err := verifyTurns.Verify(c, req.CfToken, c.ClientIP()); err != nil || !verify { - err = errors.Wrapf(xerr.NewErrCode(xerr.TooManyRequests), "error: %v, verify: %v", err, verify) - result.HttpResult(c, nil, err) - return - } - } - l := auth.NewLoginLogic(c, svcCtx) - resp, err := l.Login(&req) - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/auth/registerHandler.go b/internal/handler/app/auth/registerHandler.go deleted file mode 100644 index 44cc6e0..0000000 --- a/internal/handler/app/auth/registerHandler.go +++ /dev/null @@ -1,43 +0,0 @@ -package auth - -import ( - "time" - - "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/app/auth" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/result" - "github.com/perfect-panel/server/pkg/turnstile" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" -) - -// Register -func RegisterHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.AppAuthRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - // get client ip - if svcCtx.Config.Verify.RegisterVerify { - verifyTurns := turnstile.New(turnstile.Config{ - Secret: svcCtx.Config.Verify.TurnstileSecret, - Timeout: 3 * time.Second, - }) - if verify, err := verifyTurns.Verify(c, req.CfToken, c.ClientIP()); err != nil || !verify { - err = errors.Wrapf(xerr.NewErrCode(xerr.TooManyRequests), "error: %v, verify: %v", err, verify) - result.HttpResult(c, nil, err) - return - } - } - - l := auth.NewRegisterLogic(c, svcCtx) - resp, err := l.Register(&req) - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/auth/resetPasswordHandler.go b/internal/handler/app/auth/resetPasswordHandler.go deleted file mode 100644 index 82fbc41..0000000 --- a/internal/handler/app/auth/resetPasswordHandler.go +++ /dev/null @@ -1,41 +0,0 @@ -package auth - -import ( - "time" - - "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/app/auth" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/result" - "github.com/perfect-panel/server/pkg/turnstile" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" -) - -// Reset Password -func ResetPasswordHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.AppAuthRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - if svcCtx.Config.Verify.ResetPasswordVerify { - verifyTurns := turnstile.New(turnstile.Config{ - Secret: svcCtx.Config.Verify.TurnstileSecret, - Timeout: 3 * time.Second, - }) - if verify, err := verifyTurns.Verify(c, req.CfToken, c.ClientIP()); err != nil || !verify { - err = errors.Wrapf(xerr.NewErrCode(xerr.TooManyRequests), "error: %v, verify: %v", err, verify) - result.HttpResult(c, nil, err) - return - } - } - l := auth.NewResetPasswordLogic(c, svcCtx) - resp, err := l.ResetPassword(&req) - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/document/querydocumentlisthandler.go b/internal/handler/app/document/querydocumentlisthandler.go deleted file mode 100644 index d8cb7de..0000000 --- a/internal/handler/app/document/querydocumentlisthandler.go +++ /dev/null @@ -1,18 +0,0 @@ -package document - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/app/document" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/pkg/result" -) - -// Get document list -func QueryDocumentListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - - l := document.NewQueryDocumentListLogic(c.Request.Context(), svcCtx) - resp, err := l.QueryDocumentList() - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/node/getRuleGroupListHandler.go b/internal/handler/app/node/getRuleGroupListHandler.go deleted file mode 100644 index e48750e..0000000 --- a/internal/handler/app/node/getRuleGroupListHandler.go +++ /dev/null @@ -1,18 +0,0 @@ -package node - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/app/node" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/pkg/result" -) - -// Get rule group list -func GetRuleGroupListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - - l := node.NewGetRuleGroupListLogic(c.Request.Context(), svcCtx) - resp, err := l.GetRuleGroupList() - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/order/purchasehandler.go b/internal/handler/app/order/purchasehandler.go deleted file mode 100644 index 6aded64..0000000 --- a/internal/handler/app/order/purchasehandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package order - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/app/order" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/result" -) - -// purchase Subscription -func PurchaseHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.PurchaseOrderRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := order.NewPurchaseLogic(c.Request.Context(), svcCtx) - resp, err := l.Purchase(&req) - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/order/rechargehandler.go b/internal/handler/app/order/rechargehandler.go deleted file mode 100644 index 9d0a23e..0000000 --- a/internal/handler/app/order/rechargehandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package order - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/app/order" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/result" -) - -// Recharge -func RechargeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.RechargeOrderRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := order.NewRechargeLogic(c.Request.Context(), svcCtx) - resp, err := l.Recharge(&req) - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/order/renewalhandler.go b/internal/handler/app/order/renewalhandler.go deleted file mode 100644 index 2de271e..0000000 --- a/internal/handler/app/order/renewalhandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package order - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/app/order" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/result" -) - -// Renewal Subscription -func RenewalHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.RenewalOrderRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := order.NewRenewalLogic(c.Request.Context(), svcCtx) - resp, err := l.Renewal(&req) - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/order/resettraffichandler.go b/internal/handler/app/order/resettraffichandler.go deleted file mode 100644 index 3740794..0000000 --- a/internal/handler/app/order/resettraffichandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package order - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/app/order" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/result" -) - -// Reset traffic -func ResetTrafficHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.ResetTrafficOrderRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := order.NewResetTrafficLogic(c.Request.Context(), svcCtx) - resp, err := l.ResetTraffic(&req) - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/payment/getavailablepaymentmethodshandler.go b/internal/handler/app/payment/getavailablepaymentmethodshandler.go deleted file mode 100644 index a665a33..0000000 --- a/internal/handler/app/payment/getavailablepaymentmethodshandler.go +++ /dev/null @@ -1,18 +0,0 @@ -package payment - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/app/payment" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/pkg/result" -) - -// Get available payment methods -func GetAvailablePaymentMethodsHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - - l := payment.NewGetAvailablePaymentMethodsLogic(c.Request.Context(), svcCtx) - resp, err := l.GetAvailablePaymentMethods() - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/subscribe/queryApplicationConfigHandler.go b/internal/handler/app/subscribe/queryApplicationConfigHandler.go deleted file mode 100644 index 611d2ab..0000000 --- a/internal/handler/app/subscribe/queryApplicationConfigHandler.go +++ /dev/null @@ -1,18 +0,0 @@ -package subscribe - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/app/subscribe" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/pkg/result" -) - -// Get application config -func QueryApplicationConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - - l := subscribe.NewQueryApplicationConfigLogic(c.Request.Context(), svcCtx) - resp, err := l.QueryApplicationConfig() - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/subscribe/querySubscribeGroupListHandler.go b/internal/handler/app/subscribe/querySubscribeGroupListHandler.go deleted file mode 100644 index 972713c..0000000 --- a/internal/handler/app/subscribe/querySubscribeGroupListHandler.go +++ /dev/null @@ -1,18 +0,0 @@ -package subscribe - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/app/subscribe" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/pkg/result" -) - -// Get subscribe group list -func QuerySubscribeGroupListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - - l := subscribe.NewQuerySubscribeGroupListLogic(c.Request.Context(), svcCtx) - resp, err := l.QuerySubscribeGroupList() - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/subscribe/querySubscribeListHandler.go b/internal/handler/app/subscribe/querySubscribeListHandler.go deleted file mode 100644 index 9e4d789..0000000 --- a/internal/handler/app/subscribe/querySubscribeListHandler.go +++ /dev/null @@ -1,18 +0,0 @@ -package subscribe - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/app/subscribe" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/pkg/result" -) - -// Get subscribe list -func QuerySubscribeListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - - l := subscribe.NewQuerySubscribeListLogic(c.Request.Context(), svcCtx) - resp, err := l.QuerySubscribeList() - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/subscribe/queryUserAlreadySubscribeHandler.go b/internal/handler/app/subscribe/queryUserAlreadySubscribeHandler.go deleted file mode 100644 index 3541f2b..0000000 --- a/internal/handler/app/subscribe/queryUserAlreadySubscribeHandler.go +++ /dev/null @@ -1,18 +0,0 @@ -package subscribe - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/app/subscribe" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/pkg/result" -) - -// Get Already subscribed to package -func QueryUserAlreadySubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - - l := subscribe.NewQueryUserAlreadySubscribeLogic(c.Request.Context(), svcCtx) - resp, err := l.QueryUserAlreadySubscribe() - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/subscribe/queryUserAvailableUserSubscribeHandler.go b/internal/handler/app/subscribe/queryUserAvailableUserSubscribeHandler.go deleted file mode 100644 index 0107e9d..0000000 --- a/internal/handler/app/subscribe/queryUserAvailableUserSubscribeHandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package subscribe - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/app/subscribe" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/result" -) - -// Get Available subscriptions for users -func QueryUserAvailableUserSubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.AppUserSubscribeRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := subscribe.NewQueryUserAvailableUserSubscribeLogic(c.Request.Context(), svcCtx) - resp, err := l.QueryUserAvailableUserSubscribe(&req) - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/user/getuseronlinetimestatisticshandler.go b/internal/handler/app/user/getuseronlinetimestatisticshandler.go deleted file mode 100644 index 15ad65c..0000000 --- a/internal/handler/app/user/getuseronlinetimestatisticshandler.go +++ /dev/null @@ -1,18 +0,0 @@ -package user - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/app/user" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/pkg/result" -) - -// Get user online time total -func GetUserOnlineTimeStatisticsHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - - l := user.NewGetUserOnlineTimeStatisticsLogic(c.Request.Context(), svcCtx) - resp, err := l.GetUserOnlineTimeStatistics() - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/user/queryUserInfoHandler.go b/internal/handler/app/user/queryUserInfoHandler.go deleted file mode 100644 index 5d0087a..0000000 --- a/internal/handler/app/user/queryUserInfoHandler.go +++ /dev/null @@ -1,18 +0,0 @@ -package user - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/app/user" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/pkg/result" -) - -// query user info -func QueryUserInfoHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - - l := user.NewQueryUserInfoLogic(c.Request.Context(), svcCtx) - resp, err := l.QueryUserInfo() - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/user/queryuseraffiliatehandler.go b/internal/handler/app/user/queryuseraffiliatehandler.go deleted file mode 100644 index 26349c9..0000000 --- a/internal/handler/app/user/queryuseraffiliatehandler.go +++ /dev/null @@ -1,18 +0,0 @@ -package user - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/app/user" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/pkg/result" -) - -// Query User Affiliate Count -func QueryUserAffiliateHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - - l := user.NewQueryUserAffiliateLogic(c.Request.Context(), svcCtx) - resp, err := l.QueryUserAffiliate() - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/ws/appWsHandler.go b/internal/handler/app/ws/appWsHandler.go deleted file mode 100644 index 131c416..0000000 --- a/internal/handler/app/ws/appWsHandler.go +++ /dev/null @@ -1,20 +0,0 @@ -package ws - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/app/ws" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/pkg/result" -) - -// App heartbeat -func AppWsHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - ctx := c.Request.Context() - - // Logic: App heartbeat - l := ws.NewAppWsLogic(ctx, svcCtx) - err := l.AppWs(c.Writer, c.Request, c.Param("userid"), c.Param("identifier")) - result.HttpResult(c, nil, err) - } -} diff --git a/internal/handler/app/auth/checkHandler.go b/internal/handler/auth/deviceLoginHandler.go similarity index 60% rename from internal/handler/app/auth/checkHandler.go rename to internal/handler/auth/deviceLoginHandler.go index eab22c8..6a772bf 100644 --- a/internal/handler/app/auth/checkHandler.go +++ b/internal/handler/auth/deviceLoginHandler.go @@ -2,16 +2,16 @@ package auth import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/app/auth" + "github.com/perfect-panel/server/internal/logic/auth" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/result" ) -// Check Account -func CheckHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { +// Device Login +func DeviceLoginHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - var req types.AppAuthCheckRequest + var req types.DeviceLoginRequest _ = c.ShouldBind(&req) validateErr := svcCtx.Validate(&req) if validateErr != nil { @@ -19,8 +19,8 @@ func CheckHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return } - l := auth.NewCheckLogic(c, svcCtx) - resp, err := l.Check(&req) + l := auth.NewDeviceLoginLogic(c.Request.Context(), svcCtx) + resp, err := l.DeviceLogin(&req) result.HttpResult(c, resp, err) } } diff --git a/internal/handler/auth/telephoneUserRegisterHandler.go b/internal/handler/auth/telephoneUserRegisterHandler.go index 5388e2c..45a7ba8 100644 --- a/internal/handler/auth/telephoneUserRegisterHandler.go +++ b/internal/handler/auth/telephoneUserRegisterHandler.go @@ -25,6 +25,7 @@ func TelephoneUserRegisterHandler(svcCtx *svc.ServiceContext) func(c *gin.Contex } // get client ip req.IP = c.ClientIP() + req.UserAgent = c.Request.UserAgent() if svcCtx.Config.Verify.RegisterVerify { verifyTurns := turnstile.New(turnstile.Config{ Secret: svcCtx.Config.Verify.TurnstileSecret, diff --git a/internal/handler/auth/userRegisterHandler.go b/internal/handler/auth/userRegisterHandler.go index d7a2aec..ea40223 100644 --- a/internal/handler/auth/userRegisterHandler.go +++ b/internal/handler/auth/userRegisterHandler.go @@ -20,6 +20,7 @@ func UserRegisterHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { _ = c.ShouldBind(&req) // get client ip req.IP = c.ClientIP() + req.UserAgent = c.Request.UserAgent() if svcCtx.Config.Verify.RegisterVerify { verifyTurns := turnstile.New(turnstile.Config{ Secret: svcCtx.Config.Verify.TurnstileSecret, diff --git a/internal/handler/common/getSubscriptionHandler.go b/internal/handler/common/getClientHandler.go similarity index 57% rename from internal/handler/common/getSubscriptionHandler.go rename to internal/handler/common/getClientHandler.go index 94e98e4..78d613d 100644 --- a/internal/handler/common/getSubscriptionHandler.go +++ b/internal/handler/common/getClientHandler.go @@ -7,12 +7,11 @@ import ( "github.com/perfect-panel/server/pkg/result" ) -// Get Subscription -func GetSubscriptionHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { +// Get Client +func GetClientHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - - l := common.NewGetSubscriptionLogic(c.Request.Context(), svcCtx) - resp, err := l.GetSubscription() + l := common.NewGetClientLogic(c.Request.Context(), svcCtx) + resp, err := l.GetClient() result.HttpResult(c, resp, err) } } diff --git a/internal/handler/common/getApplicationHandler.go b/internal/handler/common/heartbeatHandler.go similarity index 58% rename from internal/handler/common/getApplicationHandler.go rename to internal/handler/common/heartbeatHandler.go index 7a58ea1..d72d771 100644 --- a/internal/handler/common/getApplicationHandler.go +++ b/internal/handler/common/heartbeatHandler.go @@ -7,12 +7,12 @@ import ( "github.com/perfect-panel/server/pkg/result" ) -// Get Tos Content -func GetApplicationHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { +// Heartbeat +func HeartbeatHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - l := common.NewGetApplicationLogic(c.Request.Context(), svcCtx) - resp, err := l.GetApplication() + l := common.NewHeartbeatLogic(c.Request.Context(), svcCtx) + resp, err := l.Heartbeat() result.HttpResult(c, resp, err) } } diff --git a/internal/handler/notify/paymentNotifyHandler.go b/internal/handler/notify/paymentNotifyHandler.go index d2efcb1..cd7d8b9 100644 --- a/internal/handler/notify/paymentNotifyHandler.go +++ b/internal/handler/notify/paymentNotifyHandler.go @@ -26,7 +26,7 @@ func PaymentNotifyHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { } switch payment.ParsePlatform(platform) { - case payment.EPay: + case payment.EPay, payment.CryptoSaaS: req := &types.EPayNotifyRequest{} if err := c.ShouldBind(req); err != nil { result.HttpResult(c, nil, err) diff --git a/internal/handler/public/portal/getSubscriptionHandler.go b/internal/handler/public/portal/getSubscriptionHandler.go index 70c836d..6d7f735 100644 --- a/internal/handler/public/portal/getSubscriptionHandler.go +++ b/internal/handler/public/portal/getSubscriptionHandler.go @@ -4,14 +4,23 @@ import ( "github.com/gin-gonic/gin" "github.com/perfect-panel/server/internal/logic/public/portal" "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/result" ) // Get Subscription func GetSubscriptionHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { + var req types.GetSubscriptionRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + l := portal.NewGetSubscriptionLogic(c.Request.Context(), svcCtx) - resp, err := l.GetSubscription() + resp, err := l.GetSubscription(&req) result.HttpResult(c, resp, err) } } diff --git a/internal/handler/public/portal/purchaseCheckoutHandler.go b/internal/handler/public/portal/purchaseCheckoutHandler.go index 21c6737..0e34d38 100644 --- a/internal/handler/public/portal/purchaseCheckoutHandler.go +++ b/internal/handler/public/portal/purchaseCheckoutHandler.go @@ -8,7 +8,7 @@ import ( "github.com/perfect-panel/server/pkg/result" ) -// Purchase Checkout +// PurchaseCheckoutHandler Purchase Checkout func PurchaseCheckoutHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { var req types.CheckoutOrderRequest diff --git a/internal/handler/public/subscribe/querySubscribeListHandler.go b/internal/handler/public/subscribe/querySubscribeListHandler.go index 309c75d..6a68a46 100644 --- a/internal/handler/public/subscribe/querySubscribeListHandler.go +++ b/internal/handler/public/subscribe/querySubscribeListHandler.go @@ -4,15 +4,23 @@ import ( "github.com/gin-gonic/gin" "github.com/perfect-panel/server/internal/logic/public/subscribe" "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/result" ) -// Get subscribe list +// QuerySubscribeListHandler Get subscribe list func QuerySubscribeListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { + var req types.QuerySubscribeListRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } l := subscribe.NewQuerySubscribeListLogic(c.Request.Context(), svcCtx) - resp, err := l.QuerySubscribeList() + resp, err := l.QuerySubscribeList(&req) result.HttpResult(c, resp, err) } } diff --git a/internal/handler/public/subscribe/queryApplicationConfigHandler.go b/internal/handler/public/subscribe/queryUserSubscribeNodeListHandler.go similarity index 53% rename from internal/handler/public/subscribe/queryApplicationConfigHandler.go rename to internal/handler/public/subscribe/queryUserSubscribeNodeListHandler.go index d9847f5..16c67fa 100644 --- a/internal/handler/public/subscribe/queryApplicationConfigHandler.go +++ b/internal/handler/public/subscribe/queryUserSubscribeNodeListHandler.go @@ -7,12 +7,12 @@ import ( "github.com/perfect-panel/server/pkg/result" ) -// Get application config -func QueryApplicationConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { +// Get user subscribe node info +func QueryUserSubscribeNodeListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - l := subscribe.NewQueryApplicationConfigLogic(c.Request.Context(), svcCtx) - resp, err := l.QueryApplicationConfig() + l := subscribe.NewQueryUserSubscribeNodeListLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryUserSubscribeNodeList() result.HttpResult(c, resp, err) } } diff --git a/internal/handler/public/user/commissionWithdrawHandler.go b/internal/handler/public/user/commissionWithdrawHandler.go new file mode 100644 index 0000000..f4f244c --- /dev/null +++ b/internal/handler/public/user/commissionWithdrawHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/public/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Commission Withdraw +func CommissionWithdrawHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.CommissionWithdrawRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewCommissionWithdrawLogic(c.Request.Context(), svcCtx) + resp, err := l.CommissionWithdraw(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/user/getDeviceListHandler.go b/internal/handler/public/user/getDeviceListHandler.go new file mode 100644 index 0000000..deb2e3a --- /dev/null +++ b/internal/handler/public/user/getDeviceListHandler.go @@ -0,0 +1,18 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/public/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" +) + +// Get Device List +func GetDeviceListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := user.NewGetDeviceListLogic(c.Request.Context(), svcCtx) + resp, err := l.GetDeviceList() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/user/queryWithdrawalLogHandler.go b/internal/handler/public/user/queryWithdrawalLogHandler.go new file mode 100644 index 0000000..9f0bddc --- /dev/null +++ b/internal/handler/public/user/queryWithdrawalLogHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/public/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Query Withdrawal Log +func QueryWithdrawalLogHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.QueryWithdrawalLogListRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewQueryWithdrawalLogLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryWithdrawalLog(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/app/user/deleteAccountHandler.go b/internal/handler/public/user/unbindDeviceHandler.go similarity index 59% rename from internal/handler/app/user/deleteAccountHandler.go rename to internal/handler/public/user/unbindDeviceHandler.go index fd9d845..9429d72 100644 --- a/internal/handler/app/user/deleteAccountHandler.go +++ b/internal/handler/public/user/unbindDeviceHandler.go @@ -2,16 +2,16 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/app/user" + "github.com/perfect-panel/server/internal/logic/public/user" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/result" ) -// Delete Account -func DeleteAccountHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { +// Unbind Device +func UnbindDeviceHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - var req types.DeleteAccountRequest + var req types.UnbindDeviceRequest _ = c.ShouldBind(&req) validateErr := svcCtx.Validate(&req) if validateErr != nil { @@ -19,8 +19,8 @@ func DeleteAccountHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return } - l := user.NewDeleteAccountLogic(c.Request.Context(), svcCtx) - err := l.DeleteAccount(&req) + l := user.NewUnbindDeviceLogic(c.Request.Context(), svcCtx) + err := l.UnbindDevice(&req) result.HttpResult(c, nil, err) } } diff --git a/internal/handler/app/user/updatePasswordHandler.go b/internal/handler/public/user/updateUserRulesHandler.go similarity index 58% rename from internal/handler/app/user/updatePasswordHandler.go rename to internal/handler/public/user/updateUserRulesHandler.go index dd40bcf..e8b9a01 100644 --- a/internal/handler/app/user/updatePasswordHandler.go +++ b/internal/handler/public/user/updateUserRulesHandler.go @@ -2,16 +2,16 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/app/user" + "github.com/perfect-panel/server/internal/logic/public/user" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/result" ) -// Update Password -func UpdatePasswordHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { +// Update User Rules +func UpdateUserRulesHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - var req types.UpdatePasswordRequeset + var req types.UpdateUserRulesRequest _ = c.ShouldBind(&req) validateErr := svcCtx.Validate(&req) if validateErr != nil { @@ -19,8 +19,8 @@ func UpdatePasswordHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return } - l := user.NewUpdatePasswordLogic(c.Request.Context(), svcCtx) - err := l.UpdatePassword(&req) + l := user.NewUpdateUserRulesLogic(c.Request.Context(), svcCtx) + err := l.UpdateUserRules(&req) result.HttpResult(c, nil, err) } } diff --git a/internal/handler/admin/system/updateApplicationConfigHandler.go b/internal/handler/public/user/updateUserSubscribeNoteHandler.go similarity index 56% rename from internal/handler/admin/system/updateApplicationConfigHandler.go rename to internal/handler/public/user/updateUserSubscribeNoteHandler.go index be766f0..17b77bf 100644 --- a/internal/handler/admin/system/updateApplicationConfigHandler.go +++ b/internal/handler/public/user/updateUserSubscribeNoteHandler.go @@ -1,17 +1,17 @@ -package system +package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/logic/admin/system" + "github.com/perfect-panel/server/internal/logic/public/user" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/result" ) -// update application config -func UpdateApplicationConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { +// Update User Subscribe Note +func UpdateUserSubscribeNoteHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - var req types.ApplicationConfig + var req types.UpdateUserSubscribeNoteRequest _ = c.ShouldBind(&req) validateErr := svcCtx.Validate(&req) if validateErr != nil { @@ -19,8 +19,8 @@ func UpdateApplicationConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Cont return } - l := system.NewUpdateApplicationConfigLogic(c.Request.Context(), svcCtx) - err := l.UpdateApplicationConfig(&req) + l := user.NewUpdateUserSubscribeNoteLogic(c.Request.Context(), svcCtx) + err := l.UpdateUserSubscribeNote(&req) result.HttpResult(c, nil, err) } } diff --git a/internal/handler/routes.go b/internal/handler/routes.go index bb39dd2..e3ad493 100644 --- a/internal/handler/routes.go +++ b/internal/handler/routes.go @@ -7,11 +7,13 @@ import ( "github.com/gin-gonic/gin" adminAds "github.com/perfect-panel/server/internal/handler/admin/ads" adminAnnouncement "github.com/perfect-panel/server/internal/handler/admin/announcement" + adminApplication "github.com/perfect-panel/server/internal/handler/admin/application" adminAuthMethod "github.com/perfect-panel/server/internal/handler/admin/authMethod" adminConsole "github.com/perfect-panel/server/internal/handler/admin/console" adminCoupon "github.com/perfect-panel/server/internal/handler/admin/coupon" adminDocument "github.com/perfect-panel/server/internal/handler/admin/document" adminLog "github.com/perfect-panel/server/internal/handler/admin/log" + adminMarketing "github.com/perfect-panel/server/internal/handler/admin/marketing" adminOrder "github.com/perfect-panel/server/internal/handler/admin/order" adminPayment "github.com/perfect-panel/server/internal/handler/admin/payment" adminServer "github.com/perfect-panel/server/internal/handler/admin/server" @@ -20,15 +22,6 @@ import ( adminTicket "github.com/perfect-panel/server/internal/handler/admin/ticket" adminTool "github.com/perfect-panel/server/internal/handler/admin/tool" adminUser "github.com/perfect-panel/server/internal/handler/admin/user" - appAnnouncement "github.com/perfect-panel/server/internal/handler/app/announcement" - appAuth "github.com/perfect-panel/server/internal/handler/app/auth" - appDocument "github.com/perfect-panel/server/internal/handler/app/document" - appNode "github.com/perfect-panel/server/internal/handler/app/node" - appOrder "github.com/perfect-panel/server/internal/handler/app/order" - appPayment "github.com/perfect-panel/server/internal/handler/app/payment" - appSubscribe "github.com/perfect-panel/server/internal/handler/app/subscribe" - appUser "github.com/perfect-panel/server/internal/handler/app/user" - appWs "github.com/perfect-panel/server/internal/handler/app/ws" auth "github.com/perfect-panel/server/internal/handler/auth" authOauth "github.com/perfect-panel/server/internal/handler/auth/oauth" common "github.com/perfect-panel/server/internal/handler/common" @@ -86,6 +79,26 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { adminAnnouncementGroupRouter.GET("/list", adminAnnouncement.GetAnnouncementListHandler(serverCtx)) } + adminApplicationGroupRouter := router.Group("/v1/admin/application") + adminApplicationGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + + { + // Create subscribe application + adminApplicationGroupRouter.POST("/", adminApplication.CreateSubscribeApplicationHandler(serverCtx)) + + // Preview Template + adminApplicationGroupRouter.GET("/preview", adminApplication.PreviewSubscribeTemplateHandler(serverCtx)) + + // Update subscribe application + adminApplicationGroupRouter.PUT("/subscribe_application", adminApplication.UpdateSubscribeApplicationHandler(serverCtx)) + + // Delete subscribe application + adminApplicationGroupRouter.DELETE("/subscribe_application", adminApplication.DeleteSubscribeApplicationHandler(serverCtx)) + + // Get subscribe application list + adminApplicationGroupRouter.GET("/subscribe_application_list", adminApplication.GetSubscribeApplicationListHandler(serverCtx)) + } + adminAuthMethodGroupRouter := router.Group("/v1/admin/auth-method") adminAuthMethodGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) @@ -176,8 +189,79 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { adminLogGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) { + // Filter balance log + adminLogGroupRouter.GET("/balance/list", adminLog.FilterBalanceLogHandler(serverCtx)) + + // Filter commission log + adminLogGroupRouter.GET("/commission/list", adminLog.FilterCommissionLogHandler(serverCtx)) + + // Filter email log + adminLogGroupRouter.GET("/email/list", adminLog.FilterEmailLogHandler(serverCtx)) + + // Filter gift log + adminLogGroupRouter.GET("/gift/list", adminLog.FilterGiftLogHandler(serverCtx)) + + // Filter login log + adminLogGroupRouter.GET("/login/list", adminLog.FilterLoginLogHandler(serverCtx)) + // Get message log list adminLogGroupRouter.GET("/message/list", adminLog.GetMessageLogListHandler(serverCtx)) + + // Filter mobile log + adminLogGroupRouter.GET("/mobile/list", adminLog.FilterMobileLogHandler(serverCtx)) + + // Filter register log + adminLogGroupRouter.GET("/register/list", adminLog.FilterRegisterLogHandler(serverCtx)) + + // Filter server traffic log + adminLogGroupRouter.GET("/server/traffic/list", adminLog.FilterServerTrafficLogHandler(serverCtx)) + + // Get log setting + adminLogGroupRouter.GET("/setting", adminLog.GetLogSettingHandler(serverCtx)) + + // Update log setting + adminLogGroupRouter.POST("/setting", adminLog.UpdateLogSettingHandler(serverCtx)) + + // Filter subscribe log + adminLogGroupRouter.GET("/subscribe/list", adminLog.FilterSubscribeLogHandler(serverCtx)) + + // Filter reset subscribe log + adminLogGroupRouter.GET("/subscribe/reset/list", adminLog.FilterResetSubscribeLogHandler(serverCtx)) + + // Filter user subscribe traffic log + adminLogGroupRouter.GET("/subscribe/traffic/list", adminLog.FilterUserSubscribeTrafficLogHandler(serverCtx)) + + // Filter traffic log details + adminLogGroupRouter.GET("/traffic/details", adminLog.FilterTrafficLogDetailsHandler(serverCtx)) + } + + adminMarketingGroupRouter := router.Group("/v1/admin/marketing") + adminMarketingGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + + { + // Get batch send email task list + adminMarketingGroupRouter.GET("/email/batch/list", adminMarketing.GetBatchSendEmailTaskListHandler(serverCtx)) + + // Get pre-send email count + adminMarketingGroupRouter.POST("/email/batch/pre-send-count", adminMarketing.GetPreSendEmailCountHandler(serverCtx)) + + // Create a batch send email task + adminMarketingGroupRouter.POST("/email/batch/send", adminMarketing.CreateBatchSendEmailTaskHandler(serverCtx)) + + // Get batch send email task status + adminMarketingGroupRouter.POST("/email/batch/status", adminMarketing.GetBatchSendEmailTaskStatusHandler(serverCtx)) + + // Stop a batch send email task + adminMarketingGroupRouter.POST("/email/batch/stop", adminMarketing.StopBatchSendEmailTaskHandler(serverCtx)) + + // Create a quota task + adminMarketingGroupRouter.POST("/quota/create", adminMarketing.CreateQuotaTaskHandler(serverCtx)) + + // Query quota task list + adminMarketingGroupRouter.GET("/quota/list", adminMarketing.QueryQuotaTaskListHandler(serverCtx)) + + // Query quota task pre-count + adminMarketingGroupRouter.POST("/quota/pre-count", adminMarketing.QueryQuotaTaskPreCountHandler(serverCtx)) } adminOrderGroupRouter := router.Group("/v1/admin/order") @@ -218,56 +302,50 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { adminServerGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) { - // Update node - adminServerGroupRouter.PUT("/", adminServer.UpdateNodeHandler(serverCtx)) + // Create Server + adminServerGroupRouter.POST("/create", adminServer.CreateServerHandler(serverCtx)) - // Create node - adminServerGroupRouter.POST("/", adminServer.CreateNodeHandler(serverCtx)) + // Delete Server + adminServerGroupRouter.POST("/delete", adminServer.DeleteServerHandler(serverCtx)) - // Delete node - adminServerGroupRouter.DELETE("/", adminServer.DeleteNodeHandler(serverCtx)) + // Filter Server List + adminServerGroupRouter.GET("/list", adminServer.FilterServerListHandler(serverCtx)) - // Batch delete node - adminServerGroupRouter.DELETE("/batch", adminServer.BatchDeleteNodeHandler(serverCtx)) + // Check if there is any server or node to migrate + adminServerGroupRouter.GET("/migrate/has", adminServer.HasMigrateSeverNodeHandler(serverCtx)) - // Get node detail - adminServerGroupRouter.GET("/detail", adminServer.GetNodeDetailHandler(serverCtx)) + // Migrate server and node data to new database + adminServerGroupRouter.POST("/migrate/run", adminServer.MigrateServerNodeHandler(serverCtx)) - // Create node group - adminServerGroupRouter.POST("/group", adminServer.CreateNodeGroupHandler(serverCtx)) + // Create Node + adminServerGroupRouter.POST("/node/create", adminServer.CreateNodeHandler(serverCtx)) - // Update node group - adminServerGroupRouter.PUT("/group", adminServer.UpdateNodeGroupHandler(serverCtx)) + // Delete Node + adminServerGroupRouter.POST("/node/delete", adminServer.DeleteNodeHandler(serverCtx)) - // Delete node group - adminServerGroupRouter.DELETE("/group", adminServer.DeleteNodeGroupHandler(serverCtx)) + // Filter Node List + adminServerGroupRouter.GET("/node/list", adminServer.FilterNodeListHandler(serverCtx)) - // Batch delete node group - adminServerGroupRouter.DELETE("/group/batch", adminServer.BatchDeleteNodeGroupHandler(serverCtx)) + // Reset node sort + adminServerGroupRouter.POST("/node/sort", adminServer.ResetSortWithNodeHandler(serverCtx)) - // Get node group list - adminServerGroupRouter.GET("/group/list", adminServer.GetNodeGroupListHandler(serverCtx)) + // Toggle Node Status + adminServerGroupRouter.POST("/node/status/toggle", adminServer.ToggleNodeStatusHandler(serverCtx)) - // Get node list - adminServerGroupRouter.GET("/list", adminServer.GetNodeListHandler(serverCtx)) + // Query all node tags + adminServerGroupRouter.GET("/node/tags", adminServer.QueryNodeTagHandler(serverCtx)) - // Create rule group - adminServerGroupRouter.POST("/rule_group", adminServer.CreateRuleGroupHandler(serverCtx)) + // Update Node + adminServerGroupRouter.POST("/node/update", adminServer.UpdateNodeHandler(serverCtx)) - // Update rule group - adminServerGroupRouter.PUT("/rule_group", adminServer.UpdateRuleGroupHandler(serverCtx)) + // Get Server Protocols + adminServerGroupRouter.GET("/protocols", adminServer.GetServerProtocolsHandler(serverCtx)) - // Delete rule group - adminServerGroupRouter.DELETE("/rule_group", adminServer.DeleteRuleGroupHandler(serverCtx)) + // Reset server sort + adminServerGroupRouter.POST("/server/sort", adminServer.ResetSortWithServerHandler(serverCtx)) - // Get rule group list - adminServerGroupRouter.GET("/rule_group_list", adminServer.GetRuleGroupListHandler(serverCtx)) - - // Node sort - adminServerGroupRouter.POST("/sort", adminServer.NodeSortHandler(serverCtx)) - - // Get node tag list - adminServerGroupRouter.GET("/tag/list", adminServer.GetNodeTagListHandler(serverCtx)) + // Update Server + adminServerGroupRouter.POST("/update", adminServer.UpdateServerHandler(serverCtx)) } adminSubscribeGroupRouter := router.Group("/v1/admin/subscribe") @@ -307,6 +385,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Get subscribe list adminSubscribeGroupRouter.GET("/list", adminSubscribe.GetSubscribeListHandler(serverCtx)) + // Reset all subscribe tokens + adminSubscribeGroupRouter.POST("/reset_all_token", adminSubscribe.ResetAllSubscribeTokenHandler(serverCtx)) + // Subscribe sort adminSubscribeGroupRouter.POST("/sort", adminSubscribe.SubscribeSortHandler(serverCtx)) } @@ -315,33 +396,6 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { adminSystemGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) { - // Get application - adminSystemGroupRouter.GET("/application", adminSystem.GetApplicationHandler(serverCtx)) - - // Update application - adminSystemGroupRouter.PUT("/application", adminSystem.UpdateApplicationHandler(serverCtx)) - - // Create application - adminSystemGroupRouter.POST("/application", adminSystem.CreateApplicationHandler(serverCtx)) - - // Delete application - adminSystemGroupRouter.DELETE("/application", adminSystem.DeleteApplicationHandler(serverCtx)) - - // update application config - adminSystemGroupRouter.PUT("/application_config", adminSystem.UpdateApplicationConfigHandler(serverCtx)) - - // get application config - adminSystemGroupRouter.GET("/application_config", adminSystem.GetApplicationConfigHandler(serverCtx)) - - // Update application version - adminSystemGroupRouter.PUT("/application_version", adminSystem.UpdateApplicationVersionHandler(serverCtx)) - - // Create application version - adminSystemGroupRouter.POST("/application_version", adminSystem.CreateApplicationVersionHandler(serverCtx)) - - // Delete application - adminSystemGroupRouter.DELETE("/application_version", adminSystem.DeleteApplicationVersionHandler(serverCtx)) - // Get Currency Config adminSystemGroupRouter.GET("/currency_config", adminSystem.GetCurrencyConfigHandler(serverCtx)) @@ -357,12 +411,18 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Update invite config adminSystemGroupRouter.PUT("/invite_config", adminSystem.UpdateInviteConfigHandler(serverCtx)) + // Get Module Config + adminSystemGroupRouter.GET("/module", adminSystem.GetModuleConfigHandler(serverCtx)) + // Get node config adminSystemGroupRouter.GET("/node_config", adminSystem.GetNodeConfigHandler(serverCtx)) // Update node config adminSystemGroupRouter.PUT("/node_config", adminSystem.UpdateNodeConfigHandler(serverCtx)) + // PreView Node Multiplier + adminSystemGroupRouter.GET("/node_multiplier/preview", adminSystem.PreViewNodeMultiplierHandler(serverCtx)) + // get Privacy Policy Config adminSystemGroupRouter.GET("/privacy", adminSystem.GetPrivacyPolicyConfigHandler(serverCtx)) @@ -393,9 +453,6 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Update subscribe config adminSystemGroupRouter.PUT("/subscribe_config", adminSystem.UpdateSubscribeConfigHandler(serverCtx)) - // Get subscribe type - adminSystemGroupRouter.GET("/subscribe_type", adminSystem.GetSubscribeTypeHandler(serverCtx)) - // Get Team of Service Config adminSystemGroupRouter.GET("/tos_config", adminSystem.GetTosConfigHandler(serverCtx)) @@ -436,11 +493,17 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { adminToolGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) { + // Query IP Location + adminToolGroupRouter.GET("/ip/location", adminTool.QueryIPLocationHandler(serverCtx)) + // Get System Log adminToolGroupRouter.GET("/log", adminTool.GetSystemLogHandler(serverCtx)) // Restart System adminToolGroupRouter.GET("/restart", adminTool.RestartSystemHandler(serverCtx)) + + // Get Version + adminToolGroupRouter.GET("/version", adminTool.GetVersionHandler(serverCtx)) } adminUserGroupRouter := router.Group("/v1/admin/user") @@ -516,158 +579,15 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Get user subcribe logs adminUserGroupRouter.GET("/subscribe/logs", adminUser.GetUserSubscribeLogsHandler(serverCtx)) + // Get user subcribe reset traffic logs + adminUserGroupRouter.GET("/subscribe/reset/logs", adminUser.GetUserSubscribeResetTrafficLogsHandler(serverCtx)) + // Get user subcribe traffic logs adminUserGroupRouter.GET("/subscribe/traffic_logs", adminUser.GetUserSubscribeTrafficLogsHandler(serverCtx)) } - appAnnouncementGroupRouter := router.Group("/v1/app/announcement") - appAnnouncementGroupRouter.Use(middleware.AppMiddleware(serverCtx), middleware.AuthMiddleware(serverCtx)) - - { - // Query announcement - appAnnouncementGroupRouter.GET("/list", appAnnouncement.QueryAnnouncementHandler(serverCtx)) - } - - appAuthGroupRouter := router.Group("/v1/app/auth") - appAuthGroupRouter.Use(middleware.AppMiddleware(serverCtx)) - - { - // Check Account - appAuthGroupRouter.POST("/check", appAuth.CheckHandler(serverCtx)) - - // GetAppConfig - appAuthGroupRouter.POST("/config", appAuth.GetAppConfigHandler(serverCtx)) - - // Login - appAuthGroupRouter.POST("/login", appAuth.LoginHandler(serverCtx)) - - // Register - appAuthGroupRouter.POST("/register", appAuth.RegisterHandler(serverCtx)) - - // Reset Password - appAuthGroupRouter.POST("/reset_password", appAuth.ResetPasswordHandler(serverCtx)) - } - - appDocumentGroupRouter := router.Group("/v1/app/document") - appDocumentGroupRouter.Use(middleware.AppMiddleware(serverCtx), middleware.AuthMiddleware(serverCtx)) - - { - // Get document detail - appDocumentGroupRouter.GET("/detail", appDocument.QueryDocumentDetailHandler(serverCtx)) - - // Get document list - appDocumentGroupRouter.GET("/list", appDocument.QueryDocumentListHandler(serverCtx)) - } - - appNodeGroupRouter := router.Group("/v1/app/node") - appNodeGroupRouter.Use(middleware.AppMiddleware(serverCtx), middleware.AuthMiddleware(serverCtx)) - - { - // Get Node list - appNodeGroupRouter.GET("/list", appNode.GetNodeListHandler(serverCtx)) - - // Get rule group list - appNodeGroupRouter.GET("/rule_group_list", appNode.GetRuleGroupListHandler(serverCtx)) - } - - appOrderGroupRouter := router.Group("/v1/app/order") - appOrderGroupRouter.Use(middleware.AppMiddleware(serverCtx), middleware.AuthMiddleware(serverCtx)) - - { - // Checkout order - appOrderGroupRouter.POST("/checkout", appOrder.CheckoutOrderHandler(serverCtx)) - - // Close order - appOrderGroupRouter.POST("/close", appOrder.CloseOrderHandler(serverCtx)) - - // Get order - appOrderGroupRouter.GET("/detail", appOrder.QueryOrderDetailHandler(serverCtx)) - - // Get order list - appOrderGroupRouter.GET("/list", appOrder.QueryOrderListHandler(serverCtx)) - - // Pre create order - appOrderGroupRouter.POST("/pre", appOrder.PreCreateOrderHandler(serverCtx)) - - // purchase Subscription - appOrderGroupRouter.POST("/purchase", appOrder.PurchaseHandler(serverCtx)) - - // Recharge - appOrderGroupRouter.POST("/recharge", appOrder.RechargeHandler(serverCtx)) - - // Renewal Subscription - appOrderGroupRouter.POST("/renewal", appOrder.RenewalHandler(serverCtx)) - - // Reset traffic - appOrderGroupRouter.POST("/reset", appOrder.ResetTrafficHandler(serverCtx)) - } - - appPaymentGroupRouter := router.Group("/v1/app/payment") - appPaymentGroupRouter.Use(middleware.AppMiddleware(serverCtx), middleware.AuthMiddleware(serverCtx)) - - { - // Get available payment methods - appPaymentGroupRouter.GET("/methods", appPayment.GetAvailablePaymentMethodsHandler(serverCtx)) - } - - appSubscribeGroupRouter := router.Group("/v1/app/subscribe") - appSubscribeGroupRouter.Use(middleware.AppMiddleware(serverCtx), middleware.AuthMiddleware(serverCtx)) - - { - // Get application config - appSubscribeGroupRouter.GET("/application/config", appSubscribe.QueryApplicationConfigHandler(serverCtx)) - - // Get subscribe group list - appSubscribeGroupRouter.GET("/group/list", appSubscribe.QuerySubscribeGroupListHandler(serverCtx)) - - // Get subscribe list - appSubscribeGroupRouter.GET("/list", appSubscribe.QuerySubscribeListHandler(serverCtx)) - - // Reset user subscription period - appSubscribeGroupRouter.POST("/reset/period", appSubscribe.ResetUserSubscribePeriodHandler(serverCtx)) - - // Get Already subscribed to package - appSubscribeGroupRouter.GET("/user/already_subscribe", appSubscribe.QueryUserAlreadySubscribeHandler(serverCtx)) - - // Get Available subscriptions for users - appSubscribeGroupRouter.GET("/user/available_subscribe", appSubscribe.QueryUserAvailableUserSubscribeHandler(serverCtx)) - } - - appUserGroupRouter := router.Group("/v1/app/user") - appUserGroupRouter.Use(middleware.AppMiddleware(serverCtx), middleware.AuthMiddleware(serverCtx)) - - { - // Delete Account - appUserGroupRouter.DELETE("/account", appUser.DeleteAccountHandler(serverCtx)) - - // Query User Affiliate Count - appUserGroupRouter.GET("/affiliate/count", appUser.QueryUserAffiliateHandler(serverCtx)) - - // Query User Affiliate List - appUserGroupRouter.GET("/affiliate/list", appUser.QueryUserAffiliateListHandler(serverCtx)) - - // query user info - appUserGroupRouter.GET("/info", appUser.QueryUserInfoHandler(serverCtx)) - - // Get user online time total - appUserGroupRouter.GET("/online_time/statistics", appUser.GetUserOnlineTimeStatisticsHandler(serverCtx)) - - // Update Password - appUserGroupRouter.PUT("/password", appUser.UpdatePasswordHandler(serverCtx)) - - // Get user subcribe traffic logs - appUserGroupRouter.GET("/subscribe/traffic_logs", appUser.GetUserSubscribeTrafficLogsHandler(serverCtx)) - } - - appWsGroupRouter := router.Group("/v1/app/ws") - appWsGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) - - { - // App heartbeat - appWsGroupRouter.GET("/:userid/:identifier", appWs.AppWsHandler(serverCtx)) - } - authGroupRouter := router.Group("/v1/auth") + authGroupRouter.Use(middleware.DeviceMiddleware(serverCtx)) { // Check user is exist @@ -679,6 +599,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // User login authGroupRouter.POST("/login", auth.UserLoginHandler(serverCtx)) + // Device Login + authGroupRouter.POST("/login/device", auth.DeviceLoginHandler(serverCtx)) + // User Telephone login authGroupRouter.POST("/login/telephone", auth.TelephoneLoginHandler(serverCtx)) @@ -709,17 +632,21 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { } commonGroupRouter := router.Group("/v1/common") + commonGroupRouter.Use(middleware.DeviceMiddleware(serverCtx)) { // Get Ads commonGroupRouter.GET("/ads", common.GetAdsHandler(serverCtx)) - // Get Tos Content - commonGroupRouter.GET("/application", common.GetApplicationHandler(serverCtx)) - // Check verification code commonGroupRouter.POST("/check_verification_code", common.CheckVerificationCodeHandler(serverCtx)) + // Get Client + commonGroupRouter.GET("/client", common.GetClientHandler(serverCtx)) + + // Heartbeat + commonGroupRouter.GET("/heartbeat", common.HeartbeatHandler(serverCtx)) + // Get verification code commonGroupRouter.POST("/send_code", common.SendEmailCodeHandler(serverCtx)) @@ -740,7 +667,7 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { } publicAnnouncementGroupRouter := router.Group("/v1/public/announcement") - publicAnnouncementGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + publicAnnouncementGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx)) { // Query announcement @@ -748,7 +675,7 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { } publicDocumentGroupRouter := router.Group("/v1/public/document") - publicDocumentGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + publicDocumentGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx)) { // Get document detail @@ -759,7 +686,7 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { } publicOrderGroupRouter := router.Group("/v1/public/order") - publicOrderGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + publicOrderGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx)) { // Close order @@ -788,7 +715,7 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { } publicPaymentGroupRouter := router.Group("/v1/public/payment") - publicPaymentGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + publicPaymentGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx)) { // Get available payment methods @@ -796,6 +723,7 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { } publicPortalGroupRouter := router.Group("/v1/public/portal") + publicPortalGroupRouter.Use(middleware.DeviceMiddleware(serverCtx)) { // Purchase Checkout @@ -818,21 +746,18 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { } publicSubscribeGroupRouter := router.Group("/v1/public/subscribe") - publicSubscribeGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + publicSubscribeGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx)) { - // Get application config - publicSubscribeGroupRouter.GET("/application/config", publicSubscribe.QueryApplicationConfigHandler(serverCtx)) - - // Get subscribe group list - publicSubscribeGroupRouter.GET("/group/list", publicSubscribe.QuerySubscribeGroupListHandler(serverCtx)) - // Get subscribe list publicSubscribeGroupRouter.GET("/list", publicSubscribe.QuerySubscribeListHandler(serverCtx)) + + // Get user subscribe node info + publicSubscribeGroupRouter.GET("/node/list", publicSubscribe.QueryUserSubscribeNodeListHandler(serverCtx)) } publicTicketGroupRouter := router.Group("/v1/public/ticket") - publicTicketGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + publicTicketGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx)) { // Update ticket status @@ -852,7 +777,7 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { } publicUserGroupRouter := router.Group("/v1/public/user") - publicUserGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + publicUserGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx)) { // Query User Affiliate Count @@ -882,6 +807,12 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Query User Commission Log publicUserGroupRouter.GET("/commission_log", publicUser.QueryUserCommissionLogHandler(serverCtx)) + // Commission Withdraw + publicUserGroupRouter.POST("/commission_withdraw", publicUser.CommissionWithdrawHandler(serverCtx)) + + // Get Device List + publicUserGroupRouter.GET("/devices", publicUser.GetDeviceListHandler(serverCtx)) + // Query User Info publicUserGroupRouter.GET("/info", publicUser.QueryUserInfoHandler(serverCtx)) @@ -897,15 +828,24 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Update User Password publicUserGroupRouter.PUT("/password", publicUser.UpdateUserPasswordHandler(serverCtx)) + // Update User Rules + publicUserGroupRouter.PUT("/rules", publicUser.UpdateUserRulesHandler(serverCtx)) + // Query User Subscribe publicUserGroupRouter.GET("/subscribe", publicUser.QueryUserSubscribeHandler(serverCtx)) // Get Subscribe Log publicUserGroupRouter.GET("/subscribe_log", publicUser.GetSubscribeLogHandler(serverCtx)) + // Update User Subscribe Note + publicUserGroupRouter.PUT("/subscribe_note", publicUser.UpdateUserSubscribeNoteHandler(serverCtx)) + // Reset User Subscribe Token publicUserGroupRouter.PUT("/subscribe_token", publicUser.ResetUserSubscribeTokenHandler(serverCtx)) + // Unbind Device + publicUserGroupRouter.PUT("/unbind_device", publicUser.UnbindDeviceHandler(serverCtx)) + // Unbind OAuth publicUserGroupRouter.POST("/unbind_oauth", publicUser.UnbindOAuthHandler(serverCtx)) @@ -920,6 +860,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Verify Email publicUserGroupRouter.POST("/verify_email", publicUser.VerifyEmailHandler(serverCtx)) + + // Query Withdrawal Log + publicUserGroupRouter.GET("/withdrawal_log", publicUser.QueryWithdrawalLogHandler(serverCtx)) } serverGroupRouter := router.Group("/v1/server") @@ -941,4 +884,11 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Get user list serverGroupRouter.GET("/user", server.GetServerUserListHandler(serverCtx)) } + + serverV2GroupRouter := router.Group("/v2/server") + + { + // Get Server Protocol Config + serverV2GroupRouter.GET("/:server_id", server.QueryServerProtocolConfigHandler(serverCtx)) + } } diff --git a/internal/handler/server/queryServerProtocolConfigHandler.go b/internal/handler/server/queryServerProtocolConfigHandler.go new file mode 100644 index 0000000..a2786b9 --- /dev/null +++ b/internal/handler/server/queryServerProtocolConfigHandler.go @@ -0,0 +1,49 @@ +package server + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/server" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/result" +) + +// QueryServerProtocolConfigHandler Get Server Protocol Config +func QueryServerProtocolConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.QueryServerConfigRequest + + serverID, err := strconv.ParseInt(c.Param("server_id"), 10, 64) + if err != nil { + logger.Debugf("[QueryServerProtocolConfigHandler] - strconv.ParseInt(server_id) error: %v, Param: %s", err, c.Param("server_id")) + c.String(http.StatusBadRequest, "Invalid Params") + c.Abort() + return + } + req.ServerID = serverID + + if err = c.ShouldBindQuery(&req); err != nil { + logger.Debugf("[QueryServerProtocolConfigHandler] - ShouldBindQuery error: %v, Query: %v", err, c.Request.URL.Query()) + c.String(http.StatusBadRequest, "Invalid Params") + c.Abort() + return + } + + fmt.Printf("[QueryServerProtocolConfigHandler] - ShouldBindQuery request: %+v\n", req) + + if svcCtx.Config.Node.NodeSecret != req.SecretKey { + c.String(http.StatusUnauthorized, "Unauthorized") + c.Abort() + return + } + + l := server.NewQueryServerProtocolConfigLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryServerProtocolConfig(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/subscribe.go b/internal/handler/subscribe.go index 5fdb800..6c228ed 100644 --- a/internal/handler/subscribe.go +++ b/internal/handler/subscribe.go @@ -1,10 +1,15 @@ package handler import ( + "net/http" + "strings" + "github.com/gin-gonic/gin" "github.com/perfect-panel/server/internal/logic/subscribe" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" ) func SubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { @@ -15,11 +20,66 @@ func SubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { } else { req.Token = c.Query("token") } + ua := c.GetHeader("User-Agent") req.UA = c.Request.Header.Get("User-Agent") req.Flag = c.Query("flag") + if svcCtx.Config.Subscribe.PanDomain { + domain := c.Request.Host + domainArr := strings.Split(domain, ".") + short, err := tool.FixedUniqueString(req.Token, 8, "") + if err != nil { + logger.Errorf("[SubscribeHandler] Generate short token failed: %v", err) + c.String(http.StatusInternalServerError, "Internal Server") + c.Abort() + return + } + if short != domainArr[0] { + c.String(http.StatusForbidden, "Access denied") + c.Abort() + return + } + } + + if svcCtx.Config.Subscribe.UserAgentLimit { + if ua == "" { + c.String(http.StatusForbidden, "Access denied") + c.Abort() + return + } + clientUserAgents := tool.RemoveDuplicateElements(strings.Split(svcCtx.Config.Subscribe.UserAgentList, "\n")...) + + // query client list + clients, err := svcCtx.ClientModel.List(c.Request.Context()) + if err != nil { + logger.Errorw("[PanDomainMiddleware] Query client list failed", logger.Field("error", err.Error())) + } + for _, item := range clients { + u := strings.ToLower(item.UserAgent) + u = strings.Trim(u, " ") + clientUserAgents = append(clientUserAgents, u) + } + + var allow = false + for _, keyword := range clientUserAgents { + keyword = strings.Trim(keyword, " ") + if keyword == "" { + continue + } + if strings.Contains(strings.ToLower(ua), strings.ToLower(keyword)) { + allow = true + } + } + if !allow { + c.String(http.StatusForbidden, "Access denied") + c.Abort() + return + } + } + l := subscribe.NewSubscribeLogic(c, svcCtx) - resp, err := l.Generate(&req) + resp, err := l.Handler(&req) if err != nil { + c.String(http.StatusInternalServerError, "Internal Server") return } c.Header("subscription-userinfo", resp.Header) @@ -30,7 +90,7 @@ func SubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { func RegisterSubscribeHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { path := serverCtx.Config.Subscribe.SubscribePath if path == "" { - path = "/api/subscribe" + path = "/v1/subscribe/config" } router.GET(path, SubscribeHandler(serverCtx)) } diff --git a/internal/logic/admin/application/createSubscribeApplicationLogic.go b/internal/logic/admin/application/createSubscribeApplicationLogic.go new file mode 100644 index 0000000..4fca252 --- /dev/null +++ b/internal/logic/admin/application/createSubscribeApplicationLogic.go @@ -0,0 +1,61 @@ +package application + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/client" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type CreateSubscribeApplicationLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewCreateSubscribeApplicationLogic Create subscribe application +func NewCreateSubscribeApplicationLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateSubscribeApplicationLogic { + return &CreateSubscribeApplicationLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CreateSubscribeApplicationLogic) CreateSubscribeApplication(req *types.CreateSubscribeApplicationRequest) (resp *types.SubscribeApplication, err error) { + var link client.DownloadLink + tool.DeepCopy(&link, req.DownloadLink) + linkData, err := link.Marshal() + if err != nil { + l.Errorf("Failed to marshal download link: %v", err) + return nil, errors.Wrap(xerr.NewErrCode(xerr.ERROR), " Failed to marshal download link") + } + data := &client.SubscribeApplication{ + Name: req.Name, + Icon: req.Icon, + Description: req.Description, + Scheme: req.Scheme, + UserAgent: req.UserAgent, + IsDefault: req.IsDefault, + SubscribeTemplate: req.SubscribeTemplate, + OutputFormat: req.OutputFormat, + DownloadLink: string(linkData), + } + + err = l.svcCtx.ClientModel.Insert(l.ctx, data) + if err != nil { + l.Errorf("Failed to create subscribe application: %v", err) + return nil, errors.Wrap(xerr.NewErrCode(xerr.DatabaseInsertError), "Failed to create subscribe application") + } + + resp = &types.SubscribeApplication{} + tool.DeepCopy(resp, data) + resp.DownloadLink = req.DownloadLink + + return +} diff --git a/internal/logic/admin/application/deleteSubscribeApplicationLogic.go b/internal/logic/admin/application/deleteSubscribeApplicationLogic.go new file mode 100644 index 0000000..57cdd70 --- /dev/null +++ b/internal/logic/admin/application/deleteSubscribeApplicationLogic.go @@ -0,0 +1,35 @@ +package application + +import ( + "context" + + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type DeleteSubscribeApplicationLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewDeleteSubscribeApplicationLogic Delete subscribe application +func NewDeleteSubscribeApplicationLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteSubscribeApplicationLogic { + return &DeleteSubscribeApplicationLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *DeleteSubscribeApplicationLogic) DeleteSubscribeApplication(req *types.DeleteSubscribeApplicationRequest) error { + err := l.svcCtx.ClientModel.Delete(l.ctx, req.Id) + if err != nil { + l.Errorf("Failed to delete subscribe application with ID %d: %v", req.Id, err) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), err.Error()) + } + return nil +} diff --git a/internal/logic/admin/application/getSubscribeApplicationListLogic.go b/internal/logic/admin/application/getSubscribeApplicationListLogic.go new file mode 100644 index 0000000..383b2b6 --- /dev/null +++ b/internal/logic/admin/application/getSubscribeApplicationListLogic.go @@ -0,0 +1,61 @@ +package application + +import ( + "context" + "encoding/json" + + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetSubscribeApplicationListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewGetSubscribeApplicationListLogic Get subscribe application list +func NewGetSubscribeApplicationListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetSubscribeApplicationListLogic { + return &GetSubscribeApplicationListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetSubscribeApplicationListLogic) GetSubscribeApplicationList(req *types.GetSubscribeApplicationListRequest) (resp *types.GetSubscribeApplicationListResponse, err error) { + data, err := l.svcCtx.ClientModel.List(l.ctx) + if err != nil { + l.Errorf("Failed to get subscribe application list: %v", err) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Failed to get subscribe application list") + } + var list []types.SubscribeApplication + for _, item := range data { + var temp types.DownloadLink + if item.DownloadLink != "" { + _ = json.Unmarshal([]byte(item.DownloadLink), &temp) + } + list = append(list, types.SubscribeApplication{ + Id: item.Id, + Name: item.Name, + Description: item.Description, + Icon: item.Icon, + Scheme: item.Scheme, + UserAgent: item.UserAgent, + IsDefault: item.IsDefault, + SubscribeTemplate: item.SubscribeTemplate, + OutputFormat: item.OutputFormat, + DownloadLink: temp, + CreatedAt: item.CreatedAt.UnixMilli(), + UpdatedAt: item.UpdatedAt.UnixMilli(), + }) + } + resp = &types.GetSubscribeApplicationListResponse{ + Total: int64(len(list)), + List: list, + } + return +} diff --git a/internal/logic/admin/application/previewSubscribeTemplateLogic.go b/internal/logic/admin/application/previewSubscribeTemplateLogic.go new file mode 100644 index 0000000..8ca72e0 --- /dev/null +++ b/internal/logic/admin/application/previewSubscribeTemplateLogic.go @@ -0,0 +1,76 @@ +package application + +import ( + "context" + "time" + + "github.com/perfect-panel/server/adapter" + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type PreviewSubscribeTemplateLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Preview Template +func NewPreviewSubscribeTemplateLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PreviewSubscribeTemplateLogic { + return &PreviewSubscribeTemplateLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *PreviewSubscribeTemplateLogic) PreviewSubscribeTemplate(req *types.PreviewSubscribeTemplateRequest) (resp *types.PreviewSubscribeTemplateResponse, err error) { + enable := true + _, servers, err := l.svcCtx.NodeModel.FilterNodeList(l.ctx, &node.FilterNodeParams{ + Page: 1, + Size: 1000, + Preload: true, + Enabled: &enable, + }) + if err != nil { + l.Errorf("[PreviewSubscribeTemplateLogic] FindAllServer error: %v", err.Error()) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindAllServer error: %v", err.Error()) + } + + data, err := l.svcCtx.ClientModel.FindOne(l.ctx, req.Id) + if err != nil { + l.Errorf("[PreviewSubscribeTemplateLogic] FindOne error: %v", err.Error()) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindOneClient error: %v", err.Error()) + } + + sub := adapter.NewAdapter(data.SubscribeTemplate, adapter.WithServers(servers), + adapter.WithSiteName("PerfectPanel"), + adapter.WithSubscribeName("Test Subscribe"), + adapter.WithOutputFormat(data.OutputFormat), + adapter.WithUserInfo(adapter.User{ + Password: "test-password", + ExpiredAt: time.Now().AddDate(1, 0, 0), + Download: 0, + Upload: 0, + Traffic: 1000, + SubscribeURL: "https://example.com/subscribe", + })) + // Get client config + a, err := sub.Client() + if err != nil { + l.Errorf("[PreviewSubscribeTemplateLogic] Client error: %v", err.Error()) + return nil, errors.Wrapf(xerr.NewErrMsg(err.Error()), "Client error: %v", err.Error()) + } + bytes, err := a.Build() + if err != nil { + l.Errorf("[PreviewSubscribeTemplateLogic] Build error: %v", err.Error()) + return nil, errors.Wrapf(xerr.NewErrMsg(err.Error()), "Build error: %v", err.Error()) + } + return &types.PreviewSubscribeTemplateResponse{ + Template: string(bytes), + }, nil +} diff --git a/internal/logic/admin/application/updateSubscribeApplicationLogic.go b/internal/logic/admin/application/updateSubscribeApplicationLogic.go new file mode 100644 index 0000000..b209768 --- /dev/null +++ b/internal/logic/admin/application/updateSubscribeApplicationLogic.go @@ -0,0 +1,62 @@ +package application + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/client" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type UpdateSubscribeApplicationLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewUpdateSubscribeApplicationLogic Update subscribe application +func NewUpdateSubscribeApplicationLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateSubscribeApplicationLogic { + return &UpdateSubscribeApplicationLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateSubscribeApplicationLogic) UpdateSubscribeApplication(req *types.UpdateSubscribeApplicationRequest) (resp *types.SubscribeApplication, err error) { + data, err := l.svcCtx.ClientModel.FindOne(l.ctx, req.Id) + if err != nil { + l.Errorf("Failed to find subscribe application with ID %d: %v", req.Id, err) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Failed to find subscribe application with ID %d", req.Id) + } + var link client.DownloadLink + tool.DeepCopy(&link, req.DownloadLink) + linkData, err := link.Marshal() + if err != nil { + l.Errorf("Failed to marshal download link: %v", err) + return nil, errors.Wrap(xerr.NewErrCode(xerr.ERROR), " Failed to marshal download link") + } + + data.Name = req.Name + data.Icon = req.Icon + data.Description = req.Description + data.Scheme = req.Scheme + data.UserAgent = req.UserAgent + data.IsDefault = req.IsDefault + data.SubscribeTemplate = req.SubscribeTemplate + data.OutputFormat = req.OutputFormat + data.DownloadLink = string(linkData) + err = l.svcCtx.ClientModel.Update(l.ctx, data) + if err != nil { + l.Errorf("Failed to update subscribe application with ID %d: %v", req.Id, err) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "Failed to update subscribe application with ID %d", req.Id) + } + resp = &types.SubscribeApplication{} + tool.DeepCopy(&resp, data) + resp.DownloadLink = req.DownloadLink + return +} diff --git a/internal/logic/admin/authMethod/updateAuthMethodConfigLogic.go b/internal/logic/admin/authMethod/updateAuthMethodConfigLogic.go index c20a45f..d61e38f 100644 --- a/internal/logic/admin/authMethod/updateAuthMethodConfigLogic.go +++ b/internal/logic/admin/authMethod/updateAuthMethodConfigLogic.go @@ -92,6 +92,9 @@ func (l *UpdateAuthMethodConfigLogic) UpdateGlobal(method string) { if method == "mobile" { initialize.Mobile(l.svcCtx) } + if method == "device" { + initialize.Device(l.svcCtx) + } } func validatePlatformConfig(platform string, cfg map[string]interface{}) (interface{}, error) { diff --git a/internal/logic/admin/console/queryRevenueStatisticsLogic.go b/internal/logic/admin/console/queryRevenueStatisticsLogic.go index 9412b76..f9dbb37 100644 --- a/internal/logic/admin/console/queryRevenueStatisticsLogic.go +++ b/internal/logic/admin/console/queryRevenueStatisticsLogic.go @@ -50,8 +50,8 @@ func (l *QueryRevenueStatisticsLogic) QueryRevenueStatistics() (resp *types.Reve // Get monthly's revenue statistics monthlyData, err := l.svcCtx.OrderModel.QueryMonthlyOrders(l.ctx, now) if err != nil { - l.Errorw("[QueryRevenueStatisticsLogic] QueryDateOrders error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "QueryDateOrders error: %v", err) + l.Errorw("[QueryRevenueStatisticsLogic] QueryMonthlyOrders error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "QueryMonthlyOrders error: %v", err) } else { monthly = types.OrdersStatistics{ AmountTotal: monthlyData.AmountTotal, @@ -61,6 +61,24 @@ func (l *QueryRevenueStatisticsLogic) QueryRevenueStatistics() (resp *types.Reve } } + // Get monthly daily list for the current month (from 1st to current date) + monthlyListData, err := l.svcCtx.OrderModel.QueryDailyOrdersList(l.ctx, now) + if err != nil { + l.Errorw("[QueryRevenueStatisticsLogic] QueryDailyOrdersList error", logger.Field("error", err.Error())) + // Don't return error, just log it and continue with empty list + } else { + monthlyList := make([]types.OrdersStatistics, len(monthlyListData)) + for i, data := range monthlyListData { + monthlyList[i] = types.OrdersStatistics{ + Date: data.Date, + AmountTotal: data.AmountTotal, + NewOrderAmount: data.NewOrderAmount, + RenewalOrderAmount: data.RenewalOrderAmount, + } + } + monthly.List = monthlyList + } + // Get all revenue statistics allData, err := l.svcCtx.OrderModel.QueryTotalOrders(l.ctx) if err != nil { @@ -74,6 +92,25 @@ func (l *QueryRevenueStatisticsLogic) QueryRevenueStatistics() (resp *types.Reve List: make([]types.OrdersStatistics, 0), } } + + // Get all monthly list for the past 6 months + allListData, err := l.svcCtx.OrderModel.QueryMonthlyOrdersList(l.ctx, now) + if err != nil { + l.Errorw("[QueryRevenueStatisticsLogic] QueryMonthlyOrdersList error", logger.Field("error", err.Error())) + // Don't return error, just log it and continue with empty list + } else { + allList := make([]types.OrdersStatistics, len(allListData)) + for i, data := range allListData { + allList[i] = types.OrdersStatistics{ + Date: data.Date, + AmountTotal: data.AmountTotal, + NewOrderAmount: data.NewOrderAmount, + RenewalOrderAmount: data.RenewalOrderAmount, + } + } + all.List = allList + } + return &types.RevenueStatisticsResponse{ Today: today, Monthly: monthly, @@ -85,7 +122,7 @@ func (l *QueryRevenueStatisticsLogic) QueryRevenueStatistics() (resp *types.Reve func (l *QueryRevenueStatisticsLogic) mockRevenueStatistics() *types.RevenueStatisticsResponse { now := time.Now() - // Generate daily data for the past 7 days (oldest first) + // Generate daily data for the current month (from 1st to current date) monthlyList := make([]types.OrdersStatistics, 7) for i := 0; i < 7; i++ { dayDate := now.AddDate(0, 0, -(6 - i)) diff --git a/internal/logic/admin/console/queryServerTotalDataLogic.go b/internal/logic/admin/console/queryServerTotalDataLogic.go index ac7a5f8..5bdafca 100644 --- a/internal/logic/admin/console/queryServerTotalDataLogic.go +++ b/internal/logic/admin/console/queryServerTotalDataLogic.go @@ -6,12 +6,15 @@ import ( "strings" "time" - "github.com/perfect-panel/server/pkg/xerr" - + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/internal/model/traffic" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" + "gorm.io/gorm" ) type QueryServerTotalDataLogic struct { @@ -35,127 +38,194 @@ func (l *QueryServerTotalDataLogic) QueryServerTotalData() (resp *types.ServerTo return l.mockRevenueStatistics(), nil } - resp = &types.ServerTotalDataResponse{ - ServerTrafficRankingToday: make([]types.ServerTrafficData, 0), - ServerTrafficRankingYesterday: make([]types.ServerTrafficData, 0), - UserTrafficRankingToday: make([]types.UserTrafficData, 0), - UserTrafficRankingYesterday: make([]types.UserTrafficData, 0), + now := time.Now() + + todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) + todayEnd := todayStart.Add(24 * time.Hour).Add(-time.Second) + query := l.svcCtx.DB.WithContext(l.ctx) + var todayTop10User []log.UserTraffic + + err = query.Model(&traffic.TrafficLog{}). + Select("user_id, subscribe_id, SUM(download + upload) AS total, SUM(download) AS download, SUM(upload) AS upload"). + Where("timestamp BETWEEN ? AND ?", todayStart, todayEnd). + Group("user_id, subscribe_id"). + Order("total DESC"). + Limit(10). + Scan(&todayTop10User).Error + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + logger.Errorf("[Traffic Stat Queue] Query user traffic failed: %v", err.Error()) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), " Query user traffic failed: %v", err.Error()) + } + var userTodayTrafficRanking []types.UserTrafficData + for _, item := range todayTop10User { + userTodayTrafficRanking = append(userTodayTrafficRanking, types.UserTrafficData{ + SID: item.SubscribeId, + Upload: item.Upload, + Download: item.Download, + }) } - // Query node server status - servers, err := l.svcCtx.ServerModel.FindAllServer(l.ctx) - if err != nil { - l.Errorw("[QueryServerTotalDataLogic] FindAllServer error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(err, "FindAllServer error: %v", err) - } - onlineServers, err := l.svcCtx.NodeCache.GetOnlineNodeStatusCount(l.ctx) - if err != nil { - l.Errorw("[QueryServerTotalDataLogic] GetOnlineNodeStatusCount error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(err, "GetOnlineNodeStatusCount error: %v", err) - } - resp.OnlineServers = onlineServers - resp.OfflineServers = int64(len(servers) - int(onlineServers)) + // query yesterday user traffic rank log + yesterday := todayStart.Add(-24 * time.Hour).Format(time.DateOnly) - // 获取所有节点在线用户 - allNodeOnlineUser, err := l.svcCtx.NodeCache.GetAllNodeOnlineUser(l.ctx) - if err != nil { - l.Errorw("[QueryServerTotalDataLogic] Get all node online user failed", logger.Field("error", err.Error())) + var yesterdayLog log.SystemLog + err = query.Model(&log.SystemLog{}).Where("`date` = ? AND `type` = ?", yesterday, log.TypeUserTrafficRank).First(&yesterdayLog).Error + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + l.Errorw("[QueryServerTotalDataLogic] Query yesterday user traffic rank log error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query yesterday user traffic rank log error: %v", err) } - resp.OnlineUserIPs = int64(len(allNodeOnlineUser)) - // 获取所有节点今日上传下载流量 - allNodeUploadTraffic, err := l.svcCtx.NodeCache.GetAllNodeUploadTraffic(l.ctx) - if err != nil { - l.Errorw("[QueryServerTotalDataLogic] Get all node upload traffic failed", logger.Field("error", err.Error())) + var yesterdayUserRankData []types.UserTrafficData + if yesterdayLog.Id > 0 { + var rank log.UserTrafficRank + err = rank.Unmarshal([]byte(yesterdayLog.Content)) + if err != nil { + l.Errorw("[QueryServerTotalDataLogic] Unmarshal yesterday user traffic rank log error", logger.Field("error", err.Error())) + } + for _, v := range rank.Rank { + yesterdayUserRankData = append(yesterdayUserRankData, types.UserTrafficData{ + SID: v.SubscribeId, + Upload: v.Upload, + Download: v.Download, + }) + } } - resp.TodayUpload = allNodeUploadTraffic - allNodeDownloadTraffic, err := l.svcCtx.NodeCache.GetAllNodeDownloadTraffic(l.ctx) - if err != nil { - l.Errorw("[QueryServerTotalDataLogic] Get all node download traffic failed", logger.Field("error", err.Error())) + + // query server traffic rank today + var todayTop10Server []log.ServerTraffic + err = query.Model(&traffic.TrafficLog{}).Select("server_id, SUM(download + upload) AS total, SUM(download) AS download, SUM(upload) AS upload"). + Where("timestamp BETWEEN ? AND ?", todayStart, todayEnd). + Group("server_id"). + Order("total DESC"). + Limit(10). + Scan(&todayTop10Server).Error + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + logger.Errorf("[Traffic Stat Queue] Query server traffic failed: %v", err.Error()) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), " Query server traffic failed: %v", err.Error()) } - resp.TodayDownload = allNodeDownloadTraffic - // 获取节点流量排行榜 前10 - nodeTrafficRankingToday, err := l.svcCtx.NodeCache.GetNodeTodayTotalTrafficRank(l.ctx, 10) - if err != nil { - l.Errorw("[QueryServerTotalDataLogic] Get node today total traffic rank failed", logger.Field("error", err.Error())) + + var todayServerRanking []types.ServerTrafficData + for _, item := range todayTop10Server { + info, err := l.svcCtx.NodeModel.FindOneServer(l.ctx, item.ServerId) + if err != nil { + l.Errorw("[QueryServerTotalDataLogic] FindOneServer error", logger.Field("error", err.Error()), logger.Field("server_id", item.ServerId)) + continue + } + todayServerRanking = append(todayServerRanking, types.ServerTrafficData{ + ServerId: item.ServerId, + Name: info.Name, + Upload: item.Upload, + Download: item.Download, + }) } - if len(nodeTrafficRankingToday) > 0 { - var serverTrafficData []types.ServerTrafficData - for _, rank := range nodeTrafficRankingToday { - serverInfo, err := l.svcCtx.ServerModel.FindOne(l.ctx, rank.ID) + + // query server traffic rank yesterday + var yesterdayTop10Server []types.ServerTrafficData + var yesterdayServerTrafficLog log.SystemLog + err = query.Model(&log.SystemLog{}).Where("`date` = ? AND `type` = ?", yesterday, log.TypeServerTrafficRank).First(&yesterdayServerTrafficLog).Error + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + l.Errorw("[QueryServerTotalDataLogic] Query yesterday server traffic rank log error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query yesterday server traffic rank log error: %v", err) + } + if yesterdayServerTrafficLog.Id > 0 { + var rank log.ServerTrafficRank + err = rank.Unmarshal([]byte(yesterdayServerTrafficLog.Content)) + if err != nil { + l.Errorw("[QueryServerTotalDataLogic] Unmarshal yesterday server traffic rank log error", logger.Field("error", err.Error())) + } + + for _, v := range rank.Rank { + info, err := l.svcCtx.NodeModel.FindOneServer(l.ctx, v.ServerId) if err != nil { - l.Errorw("[QueryServerTotalDataLogic] FindOne error", logger.Field("error", err)) + l.Errorw("[QueryServerTotalDataLogic] FindOneServer error", logger.Field("error", err.Error()), logger.Field("server_id", v.ServerId)) continue } - serverTrafficData = append(serverTrafficData, types.ServerTrafficData{ - ServerId: rank.ID, - Name: serverInfo.Name, - Upload: rank.Upload, - Download: rank.Download, + yesterdayTop10Server = append(yesterdayTop10Server, types.ServerTrafficData{ + ServerId: v.ServerId, + Name: info.Name, + Upload: v.Upload, + Download: v.Download, }) } - resp.ServerTrafficRankingToday = serverTrafficData - } - // 获取用户流量排行榜 前10 - userTrafficRankingToday, err := l.svcCtx.NodeCache.GetUserTodayTotalTrafficRank(l.ctx, 10) - if err != nil { - l.Errorw("[QueryServerTotalDataLogic] Get user today total traffic rank failed", logger.Field("error", err.Error())) } - if len(userTrafficRankingToday) > 0 { - var userTrafficData []types.UserTrafficData - for _, rank := range userTrafficRankingToday { - userTrafficData = append(userTrafficData, types.UserTrafficData{ - SID: rank.SID, - Upload: rank.Upload, - Download: rank.Download, - }) - } - resp.UserTrafficRankingToday = userTrafficData - } - // 获取昨日节点流量排行榜 前10 - nodeTrafficRankingYesterday, err := l.svcCtx.NodeCache.GetYesterdayNodeTotalTrafficRank(l.ctx) + // query online user count + onlineUsers, err := l.svcCtx.NodeModel.OnlineUserSubscribeGlobal(l.ctx) if err != nil { - l.Errorw("[QueryServerTotalDataLogic] Get yesterday node total traffic rank failed", logger.Field("error", err.Error())) - } - if len(nodeTrafficRankingYesterday) > 0 { - var serverTrafficData []types.ServerTrafficData - for _, rank := range nodeTrafficRankingYesterday { - serverTrafficData = append(serverTrafficData, types.ServerTrafficData{ - ServerId: rank.ID, - Name: rank.Name, - Upload: rank.Upload, - Download: rank.Download, - }) - } - resp.ServerTrafficRankingYesterday = serverTrafficData - } - // 获取昨日用户流量排行榜 前10 - userTrafficRankingYesterday, err := l.svcCtx.NodeCache.GetYesterdayUserTotalTrafficRank(l.ctx) - if err != nil { - l.Errorw("[QueryServerTotalDataLogic] Get yesterday user total traffic rank failed", logger.Field("error", err.Error())) - } - if len(userTrafficRankingYesterday) > 0 { - var userTrafficData []types.UserTrafficData - for _, rank := range userTrafficRankingYesterday { - userTrafficData = append(userTrafficData, types.UserTrafficData{ - SID: rank.SID, - Upload: rank.Upload, - Download: rank.Download, - }) - } - resp.UserTrafficRankingYesterday = userTrafficData + l.Errorw("[QueryServerTotalDataLogic] OnlineUserSubscribeGlobal error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "OnlineUserSubscribeGlobal error: %v", err) } - // Query node traffic by monthly - nodeTraffic, err := l.svcCtx.TrafficLogModel.QueryTrafficByMonthly(l.ctx, time.Now()) - - if err != nil { - l.Errorw("[QueryServerTotalDataLogic] QueryTrafficByMonthly error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "QueryTrafficByMonthly error: %v", err.Error()) + // query online/offline server count + var onlineServers, offlineServers int64 + err = query.Model(&node.Server{}).Where("`last_reported_at` > ?", now.Add(-5*time.Minute)).Count(&onlineServers).Error + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + l.Errorw("[QueryServerTotalDataLogic] Count online servers error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Count online servers error: %v", err) + } + + err = query.Model(&node.Server{}).Where("`last_reported_at` <= ? OR `last_reported_at` IS NULL", now.Add(-5*time.Minute)).Count(&offlineServers).Error + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + l.Errorw("[QueryServerTotalDataLogic] Count offline servers error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Count offline servers error: %v", err) + } + // TodayUpload, TodayDownload, MonthlyUpload, MonthlyDownload + var todayUpload, todayDownload, monthlyUpload, monthlyDownload int64 + + type trafficSum struct { + Upload int64 + Download int64 + } + var todayTraffic trafficSum + // Today + err = query.Model(&traffic.TrafficLog{}).Select("SUM(upload) AS upload, SUM(download) AS download"). + Where("timestamp BETWEEN ? AND ?", todayStart, todayEnd). + Scan(&todayTraffic).Error + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + l.Errorw("[QueryServerTotalDataLogic] Sum today traffic error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Sum today traffic error: %v", err) + } + todayUpload = todayTraffic.Upload + todayDownload = todayTraffic.Download + + // Monthly + monthlyUpload += todayUpload + monthlyDownload += todayDownload + + for i := now.Day() - 1; i >= 1; i-- { + var logInfo log.SystemLog + date := time.Date(now.Year(), now.Month(), i, 0, 0, 0, 0, now.Location()).Format(time.DateOnly) + err = query.Model(&log.SystemLog{}).Where("`date` = ? AND `type` = ?", date, log.TypeTrafficStat).First(&logInfo).Error + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + l.Errorw("[QueryServerTotalDataLogic] Query daily traffic stat log error", logger.Field("error", err.Error()), logger.Field("date", date)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query daily traffic stat log error: %v", err) + } + if logInfo.Id > 0 { + var stat log.TrafficStat + err = stat.Unmarshal([]byte(logInfo.Content)) + if err != nil { + l.Errorw("[QueryServerTotalDataLogic] Unmarshal daily traffic stat log error", logger.Field("error", err.Error()), logger.Field("date", date)) + continue + } + monthlyUpload += stat.Upload + monthlyDownload += stat.Download + } + } + + resp = &types.ServerTotalDataResponse{ + OnlineUsers: onlineUsers, + OnlineServers: onlineServers, + OfflineServers: offlineServers, + TodayUpload: todayUpload, + TodayDownload: todayDownload, + MonthlyUpload: monthlyUpload, + MonthlyDownload: monthlyDownload, + UpdatedAt: now.Unix(), + ServerTrafficRankingToday: todayServerRanking, + ServerTrafficRankingYesterday: yesterdayTop10Server, + UserTrafficRankingToday: userTodayTrafficRanking, + UserTrafficRankingYesterday: yesterdayUserRankData, } - resp.MonthlyUpload = nodeTraffic.Upload - resp.MonthlyDownload = nodeTraffic.Download return resp, nil } @@ -215,7 +285,7 @@ func (l *QueryServerTotalDataLogic) mockRevenueStatistics() *types.ServerTotalDa //} // return &types.ServerTotalDataResponse{ - OnlineUserIPs: 1688, + OnlineUsers: 1688, OnlineServers: 8, OfflineServers: 2, TodayUpload: 8888888888, // ~8.3GB diff --git a/internal/logic/admin/console/queryUserStatisticsLogic.go b/internal/logic/admin/console/queryUserStatisticsLogic.go index 1859c79..746176c 100644 --- a/internal/logic/admin/console/queryUserStatisticsLogic.go +++ b/internal/logic/admin/console/queryUserStatisticsLogic.go @@ -61,8 +61,24 @@ func (l *QueryUserStatisticsLogic) QueryUserStatistics() (resp *types.UserStatis } else { resp.Monthly.NewOrderUsers = newMonth resp.Monthly.RenewalOrderUsers = renewalMonth - // TODO: Check the purchase status in the past seven days - resp.Monthly.List = make([]types.UserStatistics, 0) + } + + // Get monthly daily user statistics list for the current month (from 1st to current date) + monthlyListData, err := l.svcCtx.UserModel.QueryDailyUserStatisticsList(l.ctx, now) + if err != nil { + l.Errorw("[QueryUserStatisticsLogic] QueryDailyUserStatisticsList error", logger.Field("error", err.Error())) + // Don't return error, just log it and continue with empty list + } else { + monthlyList := make([]types.UserStatistics, len(monthlyListData)) + for i, data := range monthlyListData { + monthlyList[i] = types.UserStatistics{ + Date: data.Date, + Register: data.Register, + NewOrderUsers: data.NewOrderUsers, + RenewalOrderUsers: data.RenewalOrderUsers, + } + } + resp.Monthly.List = monthlyList } // query all user count @@ -81,13 +97,32 @@ func (l *QueryUserStatisticsLogic) QueryUserStatistics() (resp *types.UserStatis resp.All.NewOrderUsers = allNewOrderUsers resp.All.RenewalOrderUsers = allRenewalOrderUsers } + + // Get all monthly user statistics list for the past 6 months + allListData, err := l.svcCtx.UserModel.QueryMonthlyUserStatisticsList(l.ctx, now) + if err != nil { + l.Errorw("[QueryUserStatisticsLogic] QueryMonthlyUserStatisticsList error", logger.Field("error", err.Error())) + // Don't return error, just log it and continue with empty list + } else { + allList := make([]types.UserStatistics, len(allListData)) + for i, data := range allListData { + allList[i] = types.UserStatistics{ + Date: data.Date, + Register: data.Register, + NewOrderUsers: data.NewOrderUsers, + RenewalOrderUsers: data.RenewalOrderUsers, + } + } + resp.All.List = allList + } + return } func (l *QueryUserStatisticsLogic) mockRevenueStatistics() *types.UserStatisticsResponse { now := time.Now() - // Generate daily user statistics for the past 7 days (oldest first) + // Generate daily user statistics for the current month (from 1st to current date) monthlyList := make([]types.UserStatistics, 7) for i := 0; i < 7; i++ { dayDate := now.AddDate(0, 0, -(6 - i)) @@ -100,6 +135,19 @@ func (l *QueryUserStatisticsLogic) mockRevenueStatistics() *types.UserStatistics } } + // Generate monthly user statistics for the past 6 months (oldest first) + allList := make([]types.UserStatistics, 6) + for i := 0; i < 6; i++ { + monthDate := now.AddDate(0, -(5 - i), 0) + baseRegister := int64(1800 + ((5 - i) * 200) + ((5-i)%2)*500) + allList[i] = types.UserStatistics{ + Date: monthDate.Format("2006-01"), + Register: baseRegister, + NewOrderUsers: int64(float64(baseRegister) * 0.65), + RenewalOrderUsers: int64(float64(baseRegister) * 0.35), + } + } + return &types.UserStatisticsResponse{ Today: types.UserStatistics{ Register: 28, @@ -116,6 +164,7 @@ func (l *QueryUserStatisticsLogic) mockRevenueStatistics() *types.UserStatistics Register: 18888, NewOrderUsers: 0, // This field is not used in All statistics RenewalOrderUsers: 0, // This field is not used in All statistics + List: allList, }, } } diff --git a/internal/logic/admin/log/filterBalanceLogLogic.go b/internal/logic/admin/log/filterBalanceLogLogic.go new file mode 100644 index 0000000..6393d66 --- /dev/null +++ b/internal/logic/admin/log/filterBalanceLogLogic.go @@ -0,0 +1,64 @@ +package log + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type FilterBalanceLogLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewFilterBalanceLogLogic Filter balance log +func NewFilterBalanceLogLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FilterBalanceLogLogic { + return &FilterBalanceLogLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *FilterBalanceLogLogic) FilterBalanceLog(req *types.FilterBalanceLogRequest) (resp *types.FilterBalanceLogResponse, err error) { + data, total, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{ + Page: req.Page, + Size: req.Size, + Type: log.TypeBalance.Uint8(), + Data: req.Date, + ObjectID: req.UserId, + }) + + if err != nil { + l.Errorw("[FilterBalanceLog] Query User Balance Log Error:", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query User Balance Log Error") + } + + list := make([]types.BalanceLog, 0) + for _, datum := range data { + var content log.Balance + if err = content.Unmarshal([]byte(datum.Content)); err != nil { + l.Errorf("[QueryUserBalanceLog] unmarshal balance log content failed: %v", err.Error()) + continue + } + list = append(list, types.BalanceLog{ + UserId: datum.ObjectID, + Amount: content.Amount, + Type: content.Type, + OrderNo: content.OrderNo, + Balance: content.Balance, + Timestamp: content.Timestamp, + }) + } + + return &types.FilterBalanceLogResponse{ + Total: total, + List: list, + }, nil +} diff --git a/internal/logic/admin/log/filterCommissionLogLogic.go b/internal/logic/admin/log/filterCommissionLogLogic.go new file mode 100644 index 0000000..6e4020d --- /dev/null +++ b/internal/logic/admin/log/filterCommissionLogLogic.go @@ -0,0 +1,61 @@ +package log + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type FilterCommissionLogLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewFilterCommissionLogLogic Filter commission log +func NewFilterCommissionLogLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FilterCommissionLogLogic { + return &FilterCommissionLogLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *FilterCommissionLogLogic) FilterCommissionLog(req *types.FilterCommissionLogRequest) (resp *types.FilterCommissionLogResponse, err error) { + data, total, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{ + Page: req.Page, + Size: req.Size, + Data: req.Date, + Type: log.TypeCommission.Uint8(), + ObjectID: req.UserId, + }) + if err != nil { + l.Errorw("Query User Commission Log failed", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query User Commission Log failed") + } + var list []types.CommissionLog + + for _, datum := range data { + var content log.Commission + if err = content.Unmarshal([]byte(datum.Content)); err != nil { + l.Errorf("unmarshal commission log content failed: %v", err.Error()) + continue + } + list = append(list, types.CommissionLog{ + UserId: datum.ObjectID, + Type: content.Type, + Amount: content.Amount, + OrderNo: content.OrderNo, + Timestamp: content.Timestamp, + }) + } + return &types.FilterCommissionLogResponse{ + Total: total, + List: list, + }, nil +} diff --git a/internal/logic/admin/log/filterEmailLogLogic.go b/internal/logic/admin/log/filterEmailLogLogic.go new file mode 100644 index 0000000..21ce204 --- /dev/null +++ b/internal/logic/admin/log/filterEmailLogLogic.go @@ -0,0 +1,68 @@ +package log + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type FilterEmailLogLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewFilterEmailLogLogic Filter email log +func NewFilterEmailLogLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FilterEmailLogLogic { + return &FilterEmailLogLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *FilterEmailLogLogic) FilterEmailLog(req *types.FilterLogParams) (resp *types.FilterEmailLogResponse, err error) { + data, total, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{ + Page: req.Page, + Size: req.Size, + Type: log.TypeEmailMessage.Uint8(), + Data: req.Date, + Search: req.Search, + }) + + if err != nil { + l.Errorf("[FilterEmailLog] failed to filter system log: %v", err.Error()) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "failed to filter system log: %v", err.Error()) + } + + var list []types.MessageLog + + for _, datum := range data { + var content log.Message + err = content.Unmarshal([]byte(datum.Content)) + if err != nil { + l.Errorf("[FilterEmailLog] failed to unmarshal content: %v", err.Error()) + continue + } + list = append(list, types.MessageLog{ + Id: datum.Id, + Type: datum.Type, + Platform: content.Platform, + To: content.To, + Subject: content.Subject, + Content: content.Content, + Status: content.Status, + CreatedAt: datum.CreatedAt.UnixMilli(), + }) + } + + return &types.FilterEmailLogResponse{ + Total: total, + List: list, + }, nil +} diff --git a/internal/logic/admin/log/filterGiftLogLogic.go b/internal/logic/admin/log/filterGiftLogLogic.go new file mode 100644 index 0000000..5b23119 --- /dev/null +++ b/internal/logic/admin/log/filterGiftLogLogic.go @@ -0,0 +1,68 @@ +package log + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type FilterGiftLogLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Filter gift log +func NewFilterGiftLogLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FilterGiftLogLogic { + return &FilterGiftLogLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *FilterGiftLogLogic) FilterGiftLog(req *types.FilterGiftLogRequest) (resp *types.FilterGiftLogResponse, err error) { + data, total, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{ + Page: req.Page, + Size: req.Size, + Type: log.TypeGift.Uint8(), + ObjectID: req.UserId, + Data: req.Date, + Search: req.Search, + }) + + if err != nil { + l.Errorf("[FilterGiftLog] failed to filter system log: %v", err.Error()) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "failed to filter system log: %v", err.Error()) + } + + var list []types.GiftLog + for _, datum := range data { + var content log.Gift + err = content.Unmarshal([]byte(datum.Content)) + if err != nil { + l.Errorf("[FilterGiftLog] failed to unmarshal content: %v", err.Error()) + continue + } + list = append(list, types.GiftLog{ + Type: content.Type, + UserId: datum.ObjectID, + OrderNo: content.OrderNo, + SubscribeId: content.SubscribeId, + Amount: content.Amount, + Balance: content.Balance, + Remark: content.Remark, + Timestamp: content.Timestamp, + }) + } + + return &types.FilterGiftLogResponse{ + Total: total, + List: list, + }, nil +} diff --git a/internal/logic/admin/log/filterLoginLogLogic.go b/internal/logic/admin/log/filterLoginLogLogic.go new file mode 100644 index 0000000..3a43941 --- /dev/null +++ b/internal/logic/admin/log/filterLoginLogLogic.go @@ -0,0 +1,65 @@ +package log + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type FilterLoginLogLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewFilterLoginLogLogic Filter login log +func NewFilterLoginLogLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FilterLoginLogLogic { + return &FilterLoginLogLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *FilterLoginLogLogic) FilterLoginLog(req *types.FilterLoginLogRequest) (resp *types.FilterLoginLogResponse, err error) { + data, total, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{ + Page: req.Page, + Size: req.Size, + Type: log.TypeLogin.Uint8(), + ObjectID: req.UserId, + Data: req.Date, + Search: req.Search, + }) + + if err != nil { + l.Errorf("[FilterLoginLog] failed to filter system log: %v", err.Error()) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "failed to filter system log: %v", err.Error()) + } + var list []types.LoginLog + for _, datum := range data { + var item log.Login + err = item.Unmarshal([]byte(datum.Content)) + if err != nil { + l.Errorf("[FilterLoginLog] failed to unmarshal content: %v", err.Error()) + continue + } + list = append(list, types.LoginLog{ + UserId: datum.ObjectID, + Method: item.Method, + LoginIP: item.LoginIP, + UserAgent: item.UserAgent, + Success: item.Success, + Timestamp: datum.CreatedAt.UnixMilli(), + }) + } + + return &types.FilterLoginLogResponse{ + Total: total, + List: list, + }, nil +} diff --git a/internal/logic/admin/log/filterMobileLogLogic.go b/internal/logic/admin/log/filterMobileLogLogic.go new file mode 100644 index 0000000..f5f0f4c --- /dev/null +++ b/internal/logic/admin/log/filterMobileLogLogic.go @@ -0,0 +1,68 @@ +package log + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type FilterMobileLogLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Filter mobile log +func NewFilterMobileLogLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FilterMobileLogLogic { + return &FilterMobileLogLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *FilterMobileLogLogic) FilterMobileLog(req *types.FilterLogParams) (resp *types.FilterMobileLogResponse, err error) { + data, total, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{ + Page: req.Page, + Size: req.Size, + Type: log.TypeMobileMessage.Uint8(), + Data: req.Date, + Search: req.Search, + }) + + if err != nil { + l.Errorf("[FilterMobileLog] failed to filter system log: %v", err.Error()) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "failed to filter system log: %v", err.Error()) + } + + var list []types.MessageLog + + for _, datum := range data { + var content log.Message + err = content.Unmarshal([]byte(datum.Content)) + if err != nil { + l.Errorf("[FilterMobileLog] failed to unmarshal content: %v", err.Error()) + continue + } + list = append(list, types.MessageLog{ + Id: datum.Id, + Type: datum.Type, + Platform: content.Platform, + To: content.To, + Subject: content.Subject, + Content: content.Content, + Status: content.Status, + CreatedAt: datum.CreatedAt.UnixMilli(), + }) + } + + return &types.FilterMobileLogResponse{ + Total: total, + List: list, + }, nil +} diff --git a/internal/logic/admin/log/filterRegisterLogLogic.go b/internal/logic/admin/log/filterRegisterLogLogic.go new file mode 100644 index 0000000..81c9684 --- /dev/null +++ b/internal/logic/admin/log/filterRegisterLogLogic.go @@ -0,0 +1,66 @@ +package log + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type FilterRegisterLogLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Filter register log +func NewFilterRegisterLogLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FilterRegisterLogLogic { + return &FilterRegisterLogLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *FilterRegisterLogLogic) FilterRegisterLog(req *types.FilterRegisterLogRequest) (resp *types.FilterRegisterLogResponse, err error) { + data, total, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{ + Page: req.Page, + Size: req.Size, + Type: log.TypeRegister.Uint8(), + ObjectID: req.UserId, + Data: req.Date, + Search: req.Search, + }) + + if err != nil { + l.Errorf("[FilterRegisterLog] failed to filter system log: %v", err.Error()) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "failed to filter system log: %v", err.Error()) + } + + var list []types.RegisterLog + for _, datum := range data { + var item log.Register + err = item.Unmarshal([]byte(datum.Content)) + if err != nil { + l.Errorf("[FilterLoginLog] failed to unmarshal content: %v", err.Error()) + continue + } + list = append(list, types.RegisterLog{ + UserId: datum.ObjectID, + AuthMethod: item.AuthMethod, + Identifier: item.Identifier, + RegisterIP: item.RegisterIP, + UserAgent: item.UserAgent, + Timestamp: item.Timestamp, + }) + } + + return &types.FilterRegisterLogResponse{ + List: list, + Total: total, + }, nil +} diff --git a/internal/logic/admin/log/filterResetSubscribeLogLogic.go b/internal/logic/admin/log/filterResetSubscribeLogLogic.go new file mode 100644 index 0000000..31e2d2a --- /dev/null +++ b/internal/logic/admin/log/filterResetSubscribeLogLogic.go @@ -0,0 +1,66 @@ +package log + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type FilterResetSubscribeLogLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewFilterResetSubscribeLogLogic Filter reset subscribe log +func NewFilterResetSubscribeLogLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FilterResetSubscribeLogLogic { + return &FilterResetSubscribeLogLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *FilterResetSubscribeLogLogic) FilterResetSubscribeLog(req *types.FilterResetSubscribeLogRequest) (resp *types.FilterResetSubscribeLogResponse, err error) { + data, total, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{ + Page: req.Page, + Size: req.Size, + Type: log.TypeResetSubscribe.Uint8(), + ObjectID: req.UserSubscribeId, + Data: req.Date, + Search: req.Search, + }) + + if err != nil { + l.Errorf("[FilterResetSubscribeLog] failed to filter system log: %v", err.Error()) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "failed to filter system log: %v", err.Error()) + } + + var list []types.ResetSubscribeLog + + for _, item := range data { + var content log.ResetSubscribe + err = content.Unmarshal([]byte(item.Content)) + if err != nil { + l.Errorf("[FilterResetSubscribeLog] failed to unmarshal content: %v", err.Error()) + continue + } + list = append(list, types.ResetSubscribeLog{ + Type: content.Type, + UserId: content.UserId, + UserSubscribeId: item.ObjectID, + OrderNo: content.OrderNo, + Timestamp: content.Timestamp, + }) + } + + return &types.FilterResetSubscribeLogResponse{ + List: list, + Total: total, + }, nil +} diff --git a/internal/logic/admin/log/filterServerTrafficLogLogic.go b/internal/logic/admin/log/filterServerTrafficLogLogic.go new file mode 100644 index 0000000..df5ce41 --- /dev/null +++ b/internal/logic/admin/log/filterServerTrafficLogLogic.go @@ -0,0 +1,166 @@ +package log + +import ( + "context" + "time" + + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/model/traffic" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type FilterServerTrafficLogLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewFilterServerTrafficLogLogic Filter server traffic log +func NewFilterServerTrafficLogLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FilterServerTrafficLogLogic { + return &FilterServerTrafficLogLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} +func (l *FilterServerTrafficLogLogic) FilterServerTrafficLog(req *types.FilterServerTrafficLogRequest) (resp *types.FilterServerTrafficLogResponse, err error) { + today := time.Now().Format("2006-01-02") + var list []types.ServerTrafficLog + var total int64 + + if req.Date == today || req.Date == "" { + now := time.Now() + start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local) + end := start.Add(24 * time.Hour).Add(-time.Nanosecond) + + var serverTraffic []log.ServerTraffic + err = l.svcCtx.DB.WithContext(l.ctx). + Model(&traffic.TrafficLog{}). + Select("server_id, SUM(download + upload) AS total, SUM(download) AS download, SUM(upload) AS upload"). + Where("timestamp BETWEEN ? AND ?", start, end). + Group("server_id"). + Order("SUM(download + upload) DESC"). + Scan(&serverTraffic).Error + if err != nil { + l.Errorw("[FilterServerTrafficLog] Query Database Error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "today traffic query error: %s", err.Error()) + } + + for _, v := range serverTraffic { + list = append(list, types.ServerTrafficLog{ + ServerId: v.ServerId, + Upload: v.Upload, + Download: v.Download, + Total: v.Total, + Date: today, + Details: true, + }) + } + + todayTotal := len(list) + + startIdx := (req.Page - 1) * req.Size + endIdx := startIdx + req.Size + + if startIdx < todayTotal { + if endIdx > todayTotal { + endIdx = todayTotal + } + pageData := list[startIdx:endIdx] + return &types.FilterServerTrafficLogResponse{ + List: pageData, + Total: int64(todayTotal), + }, nil + } + + need := endIdx - todayTotal + historyPage := (need + req.Size - 1) / req.Size // 算出需要的历史页数 + historyData, historyTotal, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{ + Page: historyPage, + Size: need, + Type: log.TypeServerTraffic.Uint8(), + }) + if err != nil { + l.Errorw("[FilterServerTrafficLog] Query History Error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "history query error: %s", err.Error()) + } + + for _, item := range historyData { + var content log.ServerTraffic + if err = content.Unmarshal([]byte(item.Content)); err != nil { + l.Errorw("[FilterServerTrafficLog] Unmarshal Error", logger.Field("error", err.Error()), logger.Field("content", item.Content)) + continue + } + + hasDetails := true + if l.svcCtx.Config.Log.AutoClear { + last := now.AddDate(0, 0, int(-l.svcCtx.Config.Log.ClearDays)) + dataTime, err := time.Parse(time.DateOnly, item.Date) + if err != nil { + l.Errorw("[FilterServerTrafficLog] Parse Date Error", logger.Field("error", err.Error()), logger.Field("date", item.Date)) + } else { + if dataTime.Before(last) { + hasDetails = false + } else { + hasDetails = true + } + } + } + + list = append(list, types.ServerTrafficLog{ + ServerId: item.ObjectID, + Upload: content.Upload, + Download: content.Download, + Total: content.Total, + Date: item.Date, + Details: hasDetails, + }) + } + + // 返回最终分页数据 + if endIdx > len(list) { + endIdx = len(list) + } + pageData := list[startIdx:endIdx] + + return &types.FilterServerTrafficLogResponse{ + List: pageData, + Total: int64(todayTotal) + historyTotal, + }, nil + } + + data, total, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{ + Page: req.Page, + Size: req.Size, + Type: log.TypeServerTraffic.Uint8(), + }) + if err != nil { + l.Errorw("[FilterServerTrafficLog] Query Database Error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "history query error: %s", err.Error()) + } + + for _, item := range data { + var content log.ServerTraffic + if err = content.Unmarshal([]byte(item.Content)); err != nil { + l.Errorw("[FilterServerTrafficLog] Unmarshal Error", logger.Field("error", err.Error()), logger.Field("content", item.Content)) + continue + } + list = append(list, types.ServerTrafficLog{ + ServerId: item.ObjectID, + Upload: content.Upload, + Download: content.Download, + Total: content.Total, + Date: item.Date, + Details: false, + }) + } + + return &types.FilterServerTrafficLogResponse{ + List: list, + Total: total, + }, nil +} diff --git a/internal/logic/admin/log/filterSubscribeLogLogic.go b/internal/logic/admin/log/filterSubscribeLogLogic.go new file mode 100644 index 0000000..560b145 --- /dev/null +++ b/internal/logic/admin/log/filterSubscribeLogLogic.go @@ -0,0 +1,71 @@ +package log + +import ( + "context" + "strconv" + + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type FilterSubscribeLogLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewFilterSubscribeLogLogic Filter subscribe log +func NewFilterSubscribeLogLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FilterSubscribeLogLogic { + return &FilterSubscribeLogLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *FilterSubscribeLogLogic) FilterSubscribeLog(req *types.FilterSubscribeLogRequest) (resp *types.FilterSubscribeLogResponse, err error) { + params := &log.FilterParams{ + Page: req.Page, + Size: req.Size, + Type: log.TypeSubscribe.Uint8(), + Data: req.Date, + ObjectID: req.UserId, + } + + if req.UserSubscribeId != 0 { + params.Search = `"user_subscribe_id":` + strconv.FormatInt(req.UserSubscribeId, 10) + } + + data, total, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, params) + if err != nil { + l.Errorf("[FilterSubscribeLog] failed to filter system log: %v", err.Error()) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "failed to filter system log") + } + + var list []types.SubscribeLog + for _, datum := range data { + var content log.Subscribe + err = content.Unmarshal([]byte(datum.Content)) + if err != nil { + l.Errorf("[FilterSubscribeLog] failed to unmarshal content: %v", err.Error()) + continue + } + list = append(list, types.SubscribeLog{ + UserId: datum.ObjectID, + Token: content.Token, + UserAgent: content.UserAgent, + ClientIP: content.ClientIP, + UserSubscribeId: content.UserSubscribeId, + Timestamp: datum.CreatedAt.UnixMilli(), + }) + } + + return &types.FilterSubscribeLogResponse{ + Total: total, + List: list, + }, nil +} diff --git a/internal/logic/admin/log/filterTrafficLogDetailsLogic.go b/internal/logic/admin/log/filterTrafficLogDetailsLogic.go new file mode 100644 index 0000000..0dea661 --- /dev/null +++ b/internal/logic/admin/log/filterTrafficLogDetailsLogic.go @@ -0,0 +1,84 @@ +package log + +import ( + "context" + "time" + + "github.com/perfect-panel/server/internal/model/traffic" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type FilterTrafficLogDetailsLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewFilterTrafficLogDetailsLogic Filter traffic log details +func NewFilterTrafficLogDetailsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FilterTrafficLogDetailsLogic { + return &FilterTrafficLogDetailsLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *FilterTrafficLogDetailsLogic) FilterTrafficLogDetails(req *types.FilterTrafficLogDetailsRequest) (resp *types.FilterTrafficLogDetailsResponse, err error) { + var start, end time.Time + if req.Date != "" { + day, err := time.ParseInLocation("2006-01-02", req.Date, time.Local) + if err != nil { + l.Errorw("[FilterTrafficLogDetails] Date Parse Error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), " date parse error: %s", err.Error()) + } + start = day + end = day.Add(24*time.Hour - time.Nanosecond) + } else { + // query today + now := time.Now() + start = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) + end = start.Add(24*time.Hour - time.Nanosecond) + } + var data []*traffic.TrafficLog + tx := l.svcCtx.DB.WithContext(l.ctx).Model(&traffic.TrafficLog{}) + if req.ServerId != 0 { + tx = tx.Where("server_id = ?", req.ServerId) + } + if !start.IsZero() && !end.IsZero() { + tx = tx.Where("timestamp BETWEEN ? AND ?", start, end) + } + if req.UserId != 0 { + tx = tx.Where("user_id = ?", req.UserId) + } + if req.SubscribeId != 0 { + tx = tx.Where("subscribe_id = ?", req.SubscribeId) + } + var total int64 + err = tx.Count(&total).Limit(req.Size).Offset((req.Page - 1) * req.Size).Find(&data).Error + if err != nil { + l.Errorw("[FilterTrafficLogDetails] Query Database Error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), " database query error: %s", err.Error()) + } + + var logs []types.TrafficLogDetails + for _, v := range data { + logs = append(logs, types.TrafficLogDetails{ + Id: v.Id, + UserId: v.UserId, + ServerId: v.ServerId, + SubscribeId: v.SubscribeId, + Download: v.Download, + Upload: v.Upload, + Timestamp: v.Timestamp.UnixMilli(), + }) + } + + return &types.FilterTrafficLogDetailsResponse{ + List: logs, + Total: total, + }, nil +} diff --git a/internal/logic/admin/log/filterUserSubscribeTrafficLogLogic.go b/internal/logic/admin/log/filterUserSubscribeTrafficLogLogic.go new file mode 100644 index 0000000..f6c5a46 --- /dev/null +++ b/internal/logic/admin/log/filterUserSubscribeTrafficLogLogic.go @@ -0,0 +1,160 @@ +package log + +import ( + "context" + "time" + + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/model/traffic" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type FilterUserSubscribeTrafficLogLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewFilterUserSubscribeTrafficLogLogic Filter user subscribe traffic log +func NewFilterUserSubscribeTrafficLogLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FilterUserSubscribeTrafficLogLogic { + return &FilterUserSubscribeTrafficLogLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *FilterUserSubscribeTrafficLogLogic) FilterUserSubscribeTrafficLog(req *types.FilterSubscribeTrafficRequest) (resp *types.FilterSubscribeTrafficResponse, err error) { + if req.Size <= 0 { + req.Size = 10 + } + if req.Page <= 0 { + req.Page = 1 + } + + today := time.Now().Format("2006-01-02") + var list []types.UserSubscribeTrafficLog + var total int64 + + if req.Date == today || req.Date == "" { + now := time.Now() + start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local) + end := start.Add(24 * time.Hour).Add(-time.Nanosecond) + + var userTraffic []types.UserSubscribeTrafficLog + err = l.svcCtx.DB.WithContext(l.ctx). + Model(&traffic.TrafficLog{}). + Select("user_id, subscribe_id, SUM(download + upload) AS total, SUM(download) AS download, SUM(upload) AS upload"). + Where("timestamp BETWEEN ? AND ?", start, end). + Group("user_id, subscribe_id"). + Order("SUM(download + upload) DESC"). + Scan(&userTraffic).Error + if err != nil { + l.Errorw("[FilterUserSubscribeTrafficLog] Query Database Error", logger.Field("error", err.Error())) + return nil, err + } + + for _, v := range userTraffic { + list = append(list, types.UserSubscribeTrafficLog{ + UserId: v.UserId, + SubscribeId: v.SubscribeId, + Upload: v.Upload, + Download: v.Download, + Total: v.Total, + Date: today, + Details: true, + }) + } + todayTotal := len(list) + + startIdx := (req.Page - 1) * req.Size + endIdx := startIdx + req.Size + if startIdx < todayTotal { + if endIdx > todayTotal { + endIdx = todayTotal + } + pageData := list[startIdx:endIdx] + return &types.FilterSubscribeTrafficResponse{ + List: pageData, + Total: int64(todayTotal), + }, nil + } + + need := endIdx - todayTotal + historyPage := (need + req.Size - 1) / req.Size // 算出需要的历史页数 + historyData, historyTotal, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{ + Page: historyPage, + Size: need, + Type: log.TypeSubscribeTraffic.Uint8(), + }) + + if err != nil { + l.Errorw("[FilterUserSubscribeTrafficLog] Query Database Error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "[FilterUserSubscribeTrafficLog] Query Database Error") + } + + for _, datum := range historyData { + var item log.UserTraffic + err = item.Unmarshal([]byte(datum.Content)) + if err != nil { + l.Errorw("[FilterUserSubscribeTrafficLog] Unmarshal Content Error", logger.Field("error", err.Error())) + continue + } + list = append(list, types.UserSubscribeTrafficLog{ + UserId: item.UserId, + SubscribeId: item.SubscribeId, + Upload: item.Upload, + Download: item.Download, + Total: item.Total, + Date: datum.Date, + Details: false, + }) + } + // 返回最终分页数据 + if endIdx > len(list) { + endIdx = len(list) + } + pageData := list[startIdx:endIdx] + + return &types.FilterSubscribeTrafficResponse{ + List: pageData, + Total: int64(todayTotal) + historyTotal, + }, nil + } + var data []*log.SystemLog + data, total, err = l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{ + Page: req.Page, + Size: req.Size, + Type: log.TypeSubscribeTraffic.Uint8(), + Data: req.Date, + }) + if err != nil { + l.Errorw("[FilterUserSubscribeTrafficLog] Query Database Error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "[FilterUserSubscribeTrafficLog] Query Database Error") + } + for _, datum := range data { + var item log.UserTraffic + err = item.Unmarshal([]byte(datum.Content)) + if err != nil { + l.Errorw("[FilterUserSubscribeTrafficLog] Unmarshal Content Error", logger.Field("error", err.Error())) + continue + } + list = append(list, types.UserSubscribeTrafficLog{ + UserId: item.UserId, + SubscribeId: item.SubscribeId, + Upload: item.Upload, + Download: item.Download, + Total: item.Total, + Date: datum.Date, + Details: false, + }) + } + return &types.FilterSubscribeTrafficResponse{ + List: list, + Total: total, + }, nil +} diff --git a/internal/logic/admin/log/getLogSettingLogic.go b/internal/logic/admin/log/getLogSettingLogic.go new file mode 100644 index 0000000..568d7e0 --- /dev/null +++ b/internal/logic/admin/log/getLogSettingLogic.go @@ -0,0 +1,37 @@ +package log + +import ( + "context" + + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" +) + +type GetLogSettingLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get log setting +func NewGetLogSettingLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetLogSettingLogic { + return &GetLogSettingLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetLogSettingLogic) GetLogSetting() (resp *types.LogSetting, err error) { + configs, err := l.svcCtx.SystemModel.GetLogConfig(l.ctx) + if err != nil { + l.Errorw("[GetLogSetting] Database query error", logger.Field("error", err.Error())) + return nil, err + } + resp = &types.LogSetting{} + // reflect to response + tool.SystemConfigSliceReflectToStruct(configs, resp) + return +} diff --git a/internal/logic/admin/log/getMessageLogListLogic.go b/internal/logic/admin/log/getMessageLogListLogic.go index b4ed00b..252028b 100644 --- a/internal/logic/admin/log/getMessageLogListLogic.go +++ b/internal/logic/admin/log/getMessageLogListLogic.go @@ -7,7 +7,6 @@ import ( "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/tool" "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -28,20 +27,39 @@ func NewGetMessageLogListLogic(ctx context.Context, svcCtx *svc.ServiceContext) } func (l *GetMessageLogListLogic) GetMessageLogList(req *types.GetMessageLogListRequest) (resp *types.GetMessageLogListResponse, err error) { - total, data, err := l.svcCtx.LogModel.FindMessageLogList(l.ctx, req.Page, req.Size, log.MessageLogFilterParams{ - Type: req.Type, - Platform: req.Platform, - To: req.To, - Subject: req.Subject, - Content: req.Content, - Status: req.Status, + + data, total, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{ + Page: req.Page, + Size: req.Size, + Type: req.Type, + Search: req.Search, }) + if err != nil { - l.Errorw("[GetMessageLogList] Database Error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "[GetMessageLogList] Database Error: %s", err.Error()) + l.Errorf("[GetMessageLogList] failed to filter system log: %v", err.Error()) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "failed to filter system log: %v", err.Error()) } + var list []types.MessageLog - tool.DeepCopy(&list, data) + + for _, datum := range data { + var content log.Message + err = content.Unmarshal([]byte(datum.Content)) + if err != nil { + l.Errorf("[GetMessageLogList] failed to unmarshal content: %v", err.Error()) + continue + } + list = append(list, types.MessageLog{ + Id: datum.Id, + Type: datum.Type, + Platform: content.Platform, + To: content.To, + Subject: content.Subject, + Content: content.Content, + Status: content.Status, + CreatedAt: datum.CreatedAt.UnixMilli(), + }) + } return &types.GetMessageLogListResponse{ Total: total, diff --git a/internal/logic/admin/log/updateLogSettingLogic.go b/internal/logic/admin/log/updateLogSettingLogic.go new file mode 100644 index 0000000..39e5846 --- /dev/null +++ b/internal/logic/admin/log/updateLogSettingLogic.go @@ -0,0 +1,63 @@ +package log + +import ( + "context" + "reflect" + + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/model/system" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type UpdateLogSettingLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewUpdateLogSettingLogic Update log setting +func NewUpdateLogSettingLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateLogSettingLogic { + return &UpdateLogSettingLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateLogSettingLogic) UpdateLogSetting(req *types.LogSetting) error { + v := reflect.ValueOf(*req) + // Get the reflection type of the structure + t := v.Type() + err := l.svcCtx.SystemModel.Transaction(l.ctx, func(db *gorm.DB) error { + var err error + for i := 0; i < v.NumField(); i++ { + // Get the field name + fieldName := t.Field(i).Name + // Get the field value to string + fieldValue := tool.ConvertValueToString(v.Field(i)) + // Update the server config + err = db.Model(&system.System{}).Where("`category` = 'log' and `key` = ?", fieldName).Update("value", fieldValue).Error + if err != nil { + break + } + } + return err + }) + if err != nil { + l.Errorw("[UpdateLogSetting] update log setting error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), " update log setting error: %v", err) + } + + l.svcCtx.Config.Log = config.Log{ + AutoClear: *req.AutoClear, + ClearDays: req.ClearDays, + } + + return nil +} diff --git a/internal/logic/admin/marketing/createBatchSendEmailTaskLogic.go b/internal/logic/admin/marketing/createBatchSendEmailTaskLogic.go new file mode 100644 index 0000000..d2f181b --- /dev/null +++ b/internal/logic/admin/marketing/createBatchSendEmailTaskLogic.go @@ -0,0 +1,170 @@ +package marketing + +import ( + "context" + "strconv" + "strings" + "time" + + "github.com/hibiken/asynq" + "github.com/perfect-panel/server/internal/model/task" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" + types2 "github.com/perfect-panel/server/queue/types" + "gorm.io/gorm" +) + +type CreateBatchSendEmailTaskLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewCreateBatchSendEmailTaskLogic Create a batch send email task +func NewCreateBatchSendEmailTaskLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateBatchSendEmailTaskLogic { + return &CreateBatchSendEmailTaskLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} +func (l *CreateBatchSendEmailTaskLogic) CreateBatchSendEmailTask(req *types.CreateBatchSendEmailTaskRequest) (err error) { + tx := l.svcCtx.DB + + var emails []string + + // 通用查询器(含 user JOIN + 注册时间范围过滤) + baseQuery := func() *gorm.DB { + query := tx.Model(&user.AuthMethods{}). + Select("auth_identifier"). + Joins("JOIN user ON user.id = user_auth_methods.user_id"). + Where("auth_type = ?", "email") + + if req.RegisterStartTime != 0 { + query = query.Where("user.created_at >= ?", time.UnixMilli(req.RegisterStartTime)) + } + if req.RegisterEndTime != 0 { + query = query.Where("user.created_at <= ?", time.UnixMilli(req.RegisterEndTime)) + } + return query + } + + var query *gorm.DB + + scope := task.ParseScopeType(req.Scope) + + switch scope { + case task.ScopeAll: + query = baseQuery() + + case task.ScopeActive: + query = baseQuery(). + Joins("JOIN user_subscribe ON user.id = user_subscribe.user_id"). + Where("user_subscribe.status IN ?", []int64{1, 2}) + + case task.ScopeExpired: + query = baseQuery(). + Joins("JOIN user_subscribe ON user.id = user_subscribe.user_id"). + Where("user_subscribe.status = ?", 3) + + case task.ScopeNone: + query = baseQuery(). + Joins("LEFT JOIN user_subscribe ON user.id = user_subscribe.user_id"). + Where("user_subscribe.user_id IS NULL") + default: + + } + if query != nil { + // 执行查询 + err = query.Pluck("auth_identifier", &emails).Error + if err != nil { + l.Errorf("[CreateBatchSendEmailTask] Failed to fetch email addresses: %v", err.Error()) + return xerr.NewErrCode(xerr.DatabaseQueryError) + } + } + + // 邮箱列表为空,返回错误 + if len(emails) == 0 && scope != task.ScopeSkip { + l.Errorf("[CreateBatchSendEmailTask] No email addresses found for the specified scope") + return xerr.NewErrMsg("No email addresses found for the specified scope") + } + + // 邮箱地址去重 + emails = tool.RemoveDuplicateElements(emails...) + + var additionalEmails []string + // 追加额外的邮箱地址(不覆盖) + if req.Additional != "" { + additionalEmails = tool.RemoveDuplicateElements(strings.Split(req.Additional, "\n")...) + } + if len(additionalEmails) == 0 && scope == task.ScopeSkip { + l.Errorf("[CreateBatchSendEmailTask] No additional email addresses provided for skip scope") + return xerr.NewErrMsg("No additional email addresses provided for skip scope") + } + + scheduledAt := time.Now().Add(10 * time.Second) // 默认延迟10秒执行,防止任务创建和执行时间过于接近 + if req.Scheduled != 0 { + scheduledAt = time.Unix(req.Scheduled, 0) + if scheduledAt.Before(time.Now()) { + scheduledAt = time.Now() + } + } + + scopeInfo := task.EmailScope{ + Type: scope.Int8(), + RegisterStartTime: req.RegisterStartTime, + RegisterEndTime: req.RegisterEndTime, + Recipients: emails, + Additional: additionalEmails, + Scheduled: req.Scheduled, + Interval: req.Interval, + Limit: req.Limit, + } + scopeBytes, _ := scopeInfo.Marshal() + + taskContent := task.EmailContent{ + Subject: req.Subject, + Content: req.Content, + } + + contentBytes, _ := taskContent.Marshal() + + var total uint64 + if additionalEmails != nil { + list := append(emails, additionalEmails...) + total = uint64(len(tool.RemoveDuplicateElements(list...))) + } else { + total = uint64(len(emails)) + } + + taskInfo := &task.Task{ + Type: task.TypeEmail, + Scope: string(scopeBytes), + Content: string(contentBytes), + Status: 0, + Errors: "", + Total: total, + Current: 0, + } + + if err = l.svcCtx.DB.Model(&task.Task{}).Create(taskInfo).Error; err != nil { + l.Errorf("[CreateBatchSendEmailTask] Failed to create email task: %v", err.Error()) + return xerr.NewErrCode(xerr.DatabaseInsertError) + } + // create task + l.Infof("[CreateBatchSendEmailTask] Successfully created email task with ID: %d", taskInfo.Id) + + t := asynq.NewTask(types2.ScheduledBatchSendEmail, []byte(strconv.FormatInt(taskInfo.Id, 10))) + info, err := l.svcCtx.Queue.EnqueueContext(l.ctx, t, asynq.ProcessAt(scheduledAt)) + if err != nil { + l.Errorf("[CreateBatchSendEmailTask] Failed to enqueue email task: %v", err.Error()) + return xerr.NewErrCode(xerr.QueueEnqueueError) + } + l.Infof("[CreateBatchSendEmailTask] Successfully enqueued email task with ID: %s, scheduled at: %s", info.ID, scheduledAt.Format(time.DateTime)) + + return nil +} diff --git a/internal/logic/admin/marketing/createQuotaTaskLogic.go b/internal/logic/admin/marketing/createQuotaTaskLogic.go new file mode 100644 index 0000000..606435f --- /dev/null +++ b/internal/logic/admin/marketing/createQuotaTaskLogic.go @@ -0,0 +1,104 @@ +package marketing + +import ( + "context" + "strconv" + "time" + + "github.com/hibiken/asynq" + "github.com/perfect-panel/server/internal/model/task" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + queueType "github.com/perfect-panel/server/queue/types" + "github.com/pkg/errors" +) + +type CreateQuotaTaskLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewCreateQuotaTaskLogic Create a quota task +func NewCreateQuotaTaskLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateQuotaTaskLogic { + return &CreateQuotaTaskLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CreateQuotaTaskLogic) CreateQuotaTask(req *types.CreateQuotaTaskRequest) error { + var subs []*user.Subscribe + query := l.svcCtx.DB.WithContext(l.ctx).Model(&user.Subscribe{}) + if len(req.Subscribers) > 0 { + query = query.Where("`subscribe_id` IN ?", req.Subscribers) + } + + if req.IsActive != nil && *req.IsActive { + query = query.Where("`status` IN ?", []int64{0, 1, 2}) // 0: Pending 1: Active 2: Finished + } + if req.StartTime != 0 { + start := time.UnixMilli(req.StartTime) + query = query.Where("`start_time` <= ?", start) + } + if req.EndTime != 0 { + end := time.UnixMilli(req.EndTime) + query = query.Where("`expire_time` >= ?", end) + } + + if err := query.Find(&subs).Error; err != nil { + l.Errorf("[CreateQuotaTask] find subscribers error: %v", err.Error()) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribers error") + } + if len(subs) == 0 { + return errors.Wrapf(xerr.NewErrMsg("No subscribers found"), "no subscribers found") + } + var subIds []int64 + for _, sub := range subs { + subIds = append(subIds, sub.Id) + } + + scopeInfo := task.QuotaScope{ + Subscribers: req.Subscribers, + IsActive: req.IsActive, + StartTime: req.StartTime, + EndTime: req.EndTime, + Objects: subIds, + } + scopeBytes, _ := scopeInfo.Marshal() + contentInfo := task.QuotaContent{ + ResetTraffic: req.ResetTraffic, + Days: req.Days, + GiftType: req.GiftType, + GiftValue: req.GiftValue, + } + contentBytes, _ := contentInfo.Marshal() + // create task + newTask := &task.Task{ + Type: task.TypeQuota, + Status: 0, + Scope: string(scopeBytes), + Content: string(contentBytes), + Total: uint64(len(subIds)), + Current: 0, + Errors: "", + } + + if err := l.svcCtx.DB.WithContext(l.ctx).Model(&task.Task{}).Create(newTask).Error; err != nil { + l.Errorf("[CreateQuotaTask] create task error: %v", err.Error()) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create task error") + } + + // enqueue task + queueTask := asynq.NewTask(queueType.ForthwithQuotaTask, []byte(strconv.FormatInt(newTask.Id, 10))) + if _, err := l.svcCtx.Queue.EnqueueContext(l.ctx, queueTask); err != nil { + l.Errorf("[CreateQuotaTask] enqueue task error: %v", err.Error()) + return errors.Wrapf(xerr.NewErrCode(xerr.QueueEnqueueError), "enqueue task error") + } + logger.Infof("[CreateQuotaTask] Successfully created task with ID: %d", newTask.Id) + return nil +} diff --git a/internal/logic/admin/marketing/getBatchSendEmailTaskListLogic.go b/internal/logic/admin/marketing/getBatchSendEmailTaskListLogic.go new file mode 100644 index 0000000..e3a3c7b --- /dev/null +++ b/internal/logic/admin/marketing/getBatchSendEmailTaskListLogic.go @@ -0,0 +1,89 @@ +package marketing + +import ( + "context" + "strings" + + "github.com/perfect-panel/server/internal/model/task" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" +) + +type GetBatchSendEmailTaskListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewGetBatchSendEmailTaskListLogic Get batch send email task list +func NewGetBatchSendEmailTaskListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetBatchSendEmailTaskListLogic { + return &GetBatchSendEmailTaskListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetBatchSendEmailTaskListLogic) GetBatchSendEmailTaskList(req *types.GetBatchSendEmailTaskListRequest) (resp *types.GetBatchSendEmailTaskListResponse, err error) { + + var tasks []*task.Task + tx := l.svcCtx.DB.Model(&task.Task{}).Where("`type` = ?", task.TypeEmail) + if req.Status != nil { + tx = tx.Where("status = ?", *req.Status) + } + if req.Scope != nil { + tx = tx.Where("scope = ?", req.Scope) + } + if req.Page == 0 { + req.Page = 1 + } + if req.Size == 0 { + req.Size = 10 + } + err = tx.Offset((req.Page - 1) * req.Size).Limit(req.Size).Order("created_at DESC").Find(&tasks).Error + if err != nil { + l.Errorf("failed to get email tasks: %v", err) + return nil, xerr.NewErrCode(xerr.DatabaseQueryError) + } + + list := make([]types.BatchSendEmailTask, 0) + + for _, t := range tasks { + var scopeInfo task.EmailScope + if err = scopeInfo.Unmarshal([]byte(t.Scope)); err != nil { + l.Errorf("[GetBatchSendEmailTaskList] failed to unmarshal email task scope: %v", err.Error()) + continue + } + var contentInfo task.EmailContent + if err = contentInfo.Unmarshal([]byte(t.Content)); err != nil { + l.Errorf("[GetBatchSendEmailTaskList] failed to unmarshal email task content: %v", err.Error()) + continue + } + + list = append(list, types.BatchSendEmailTask{ + Id: t.Id, + Subject: contentInfo.Subject, + Content: contentInfo.Content, + Recipients: strings.Join(scopeInfo.Recipients, "\n"), + Scope: scopeInfo.Type, + RegisterStartTime: scopeInfo.RegisterStartTime, + RegisterEndTime: scopeInfo.RegisterEndTime, + Additional: strings.Join(scopeInfo.Additional, "\n"), + Scheduled: scopeInfo.Scheduled, + Interval: scopeInfo.Interval, + Limit: scopeInfo.Limit, + Status: uint8(t.Status), + Errors: t.Errors, + Total: t.Total, + Current: t.Current, + CreatedAt: t.CreatedAt.UnixMilli(), + UpdatedAt: t.UpdatedAt.UnixMilli(), + }) + } + + return &types.GetBatchSendEmailTaskListResponse{ + List: list, + }, nil +} diff --git a/internal/logic/admin/marketing/getBatchSendEmailTaskStatusLogic.go b/internal/logic/admin/marketing/getBatchSendEmailTaskStatusLogic.go new file mode 100644 index 0000000..e21a7c5 --- /dev/null +++ b/internal/logic/admin/marketing/getBatchSendEmailTaskStatusLogic.go @@ -0,0 +1,44 @@ +package marketing + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/task" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" +) + +type GetBatchSendEmailTaskStatusLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewGetBatchSendEmailTaskStatusLogic Get batch send email task status +func NewGetBatchSendEmailTaskStatusLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetBatchSendEmailTaskStatusLogic { + return &GetBatchSendEmailTaskStatusLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetBatchSendEmailTaskStatusLogic) GetBatchSendEmailTaskStatus(req *types.GetBatchSendEmailTaskStatusRequest) (resp *types.GetBatchSendEmailTaskStatusResponse, err error) { + tx := l.svcCtx.DB + + var taskInfo *task.Task + err = tx.Model(&task.Task{}).Where("id = ?", req.Id).First(&taskInfo).Error + if err != nil { + l.Errorf("failed to get email task status, error: %v", err) + return nil, xerr.NewErrCode(xerr.DatabaseQueryError) + } + + return &types.GetBatchSendEmailTaskStatusResponse{ + Status: uint8(taskInfo.Status), + Total: int64(taskInfo.Total), + Current: int64(taskInfo.Current), + Errors: taskInfo.Errors, + }, nil +} diff --git a/internal/logic/admin/marketing/getPreSendEmailCountLogic.go b/internal/logic/admin/marketing/getPreSendEmailCountLogic.go new file mode 100644 index 0000000..9fbdbe4 --- /dev/null +++ b/internal/logic/admin/marketing/getPreSendEmailCountLogic.go @@ -0,0 +1,93 @@ +package marketing + +import ( + "context" + "time" + + "github.com/perfect-panel/server/internal/model/task" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "gorm.io/gorm" +) + +type GetPreSendEmailCountLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewGetPreSendEmailCountLogic Get pre-send email count +func NewGetPreSendEmailCountLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetPreSendEmailCountLogic { + return &GetPreSendEmailCountLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetPreSendEmailCountLogic) GetPreSendEmailCount(req *types.GetPreSendEmailCountRequest) (resp *types.GetPreSendEmailCountResponse, err error) { + tx := l.svcCtx.DB + var count int64 + // 通用查询器(含 user JOIN + 注册时间范围过滤) + baseQuery := func() *gorm.DB { + query := tx.Model(&user.AuthMethods{}). + Select("auth_identifier"). + Joins("JOIN user ON user.id = user_auth_methods.user_id"). + Where("auth_type = ?", "email") + + if req.RegisterStartTime != 0 { + + registerStartTime := time.UnixMilli(req.RegisterStartTime) + + query = query.Where("user.created_at >= ?", registerStartTime) + } + if req.RegisterEndTime != 0 { + registerEndTime := time.UnixMilli(req.RegisterEndTime) + query = query.Where("user.created_at <= ?", registerEndTime) + } + return query + } + var query *gorm.DB + scope := task.ParseScopeType(req.Scope) + + switch scope { + case task.ScopeAll: + query = baseQuery() + + case task.ScopeActive: + query = baseQuery(). + Joins("JOIN user_subscribe ON user.id = user_subscribe.user_id"). + Where("user_subscribe.status IN ?", []int64{1, 2}) + + case task.ScopeExpired: + query = baseQuery(). + Joins("JOIN user_subscribe ON user.id = user_subscribe.user_id"). + Where("user_subscribe.status = ?", 3) + + case task.ScopeNone: + query = baseQuery(). + Joins("LEFT JOIN user_subscribe ON user.id = user_subscribe.user_id"). + Where("user_subscribe.user_id IS NULL") + case task.ScopeSkip: + // Skip scope does not require a count + query = nil + default: + l.Errorf("[CreateBatchSendEmailTask] Invalid scope: %v", req.Scope) + return nil, xerr.NewErrMsg("Invalid email scope") + + } + + if query != nil { + if err = query.Count(&count).Error; err != nil { + l.Errorf("[GetPreSendEmailCount] Count error: %v", err) + return nil, xerr.NewErrMsg("Failed to count emails") + } + } + + return &types.GetPreSendEmailCountResponse{ + Count: count, + }, nil +} diff --git a/internal/logic/admin/marketing/queryQuotaTaskListLogic.go b/internal/logic/admin/marketing/queryQuotaTaskListLogic.go new file mode 100644 index 0000000..50cfd9f --- /dev/null +++ b/internal/logic/admin/marketing/queryQuotaTaskListLogic.go @@ -0,0 +1,83 @@ +package marketing + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/task" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" +) + +type QueryQuotaTaskListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewQueryQuotaTaskListLogic Query quota task list +func NewQueryQuotaTaskListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryQuotaTaskListLogic { + return &QueryQuotaTaskListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryQuotaTaskListLogic) QueryQuotaTaskList(req *types.QueryQuotaTaskListRequest) (resp *types.QueryQuotaTaskListResponse, err error) { + var data []*task.Task + var count int64 + query := l.svcCtx.DB.Model(&task.Task{}).Where("`type` = ?", task.TypeQuota) + if req.Page == 0 { + req.Page = 1 + } + if req.Size == 0 { + req.Size = 20 + } + + if req.Status != nil { + query = query.Where("`status` = ?", *req.Status) + } + err = query.Count(&count).Offset((req.Page - 1) * req.Size).Limit(req.Size).Order("created_at DESC").Find(&data).Error + if err != nil { + l.Errorf("[QueryQuotaTaskList] failed to get quota tasks: %v", err) + return nil, err + } + + var list []types.QuotaTask + for _, item := range data { + var scopeInfo task.QuotaScope + if err = scopeInfo.Unmarshal([]byte(item.Scope)); err != nil { + l.Errorf("[QueryQuotaTaskList] failed to unmarshal quota task scope: %v", err.Error()) + continue + } + var contentInfo task.QuotaContent + if err = contentInfo.Unmarshal([]byte(item.Content)); err != nil { + l.Errorf("[QueryQuotaTaskList] failed to unmarshal quota task content: %v", err.Error()) + continue + } + list = append(list, types.QuotaTask{ + Id: item.Id, + Subscribers: scopeInfo.Subscribers, + IsActive: scopeInfo.IsActive, + StartTime: scopeInfo.StartTime, + EndTime: scopeInfo.EndTime, + ResetTraffic: contentInfo.ResetTraffic, + Days: contentInfo.Days, + GiftType: contentInfo.GiftType, + GiftValue: contentInfo.GiftValue, + Objects: scopeInfo.Objects, + Status: uint8(item.Status), + Total: int64(item.Total), + Current: int64(item.Current), + Errors: item.Errors, + CreatedAt: item.CreatedAt.UnixMilli(), + UpdatedAt: item.UpdatedAt.UnixMilli(), + }) + } + + return &types.QueryQuotaTaskListResponse{ + Total: count, + List: list, + }, nil +} diff --git a/internal/logic/admin/marketing/queryQuotaTaskPreCountLogic.go b/internal/logic/admin/marketing/queryQuotaTaskPreCountLogic.go new file mode 100644 index 0000000..21b0cb4 --- /dev/null +++ b/internal/logic/admin/marketing/queryQuotaTaskPreCountLogic.go @@ -0,0 +1,55 @@ +package marketing + +import ( + "context" + "time" + + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" +) + +type QueryQuotaTaskPreCountLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewQueryQuotaTaskPreCountLogic Query quota task pre-count +func NewQueryQuotaTaskPreCountLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryQuotaTaskPreCountLogic { + return &QueryQuotaTaskPreCountLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryQuotaTaskPreCountLogic) QueryQuotaTaskPreCount(req *types.QueryQuotaTaskPreCountRequest) (resp *types.QueryQuotaTaskPreCountResponse, err error) { + tx := l.svcCtx.DB.WithContext(l.ctx).Model(&user.Subscribe{}) + var count int64 + + if len(req.Subscribers) > 0 { + tx = tx.Where("`subscribe_id` IN ?", req.Subscribers) + } + + if req.IsActive != nil && *req.IsActive { + tx = tx.Where("`status` IN ?", []int64{0, 1, 2}) // 0: Pending 1: Active 2: Finished + } + if req.StartTime != 0 { + start := time.UnixMilli(req.StartTime) + tx = tx.Where("`start_time` <= ?", start) + } + if req.EndTime != 0 { + end := time.UnixMilli(req.EndTime) + tx = tx.Where("`expire_time` >= ?", end) + } + if err = tx.Count(&count).Error; err != nil { + l.Errorf("[QueryQuotaTaskPreCount] count error: %v", err.Error()) + return nil, err + } + + return &types.QueryQuotaTaskPreCountResponse{ + Count: count, + }, nil +} diff --git a/internal/logic/admin/marketing/queryQuotaTaskStatusLogic.go b/internal/logic/admin/marketing/queryQuotaTaskStatusLogic.go new file mode 100644 index 0000000..70599fe --- /dev/null +++ b/internal/logic/admin/marketing/queryQuotaTaskStatusLogic.go @@ -0,0 +1,42 @@ +package marketing + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/task" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type QueryQuotaTaskStatusLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewQueryQuotaTaskStatusLogic Query quota task status +func NewQueryQuotaTaskStatusLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryQuotaTaskStatusLogic { + return &QueryQuotaTaskStatusLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryQuotaTaskStatusLogic) QueryQuotaTaskStatus(req *types.QueryQuotaTaskStatusRequest) (resp *types.QueryQuotaTaskStatusResponse, err error) { + var data *task.Task + err = l.svcCtx.DB.Model(&task.Task{}).Where("id = ? AND `type` = ?", req.Id, task.TypeQuota).First(&data).Error + if err != nil { + l.Errorf("[QueryQuotaTaskStatus] failed to get quota task: %v", err.Error()) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), " failed to get quota task: %v", err.Error()) + } + return &types.QueryQuotaTaskStatusResponse{ + Status: uint8(data.Status), + Current: int64(data.Current), + Total: int64(data.Total), + Errors: data.Errors, + }, nil +} diff --git a/internal/logic/admin/marketing/stopBatchSendEmailTaskLogic.go b/internal/logic/admin/marketing/stopBatchSendEmailTaskLogic.go new file mode 100644 index 0000000..da3949f --- /dev/null +++ b/internal/logic/admin/marketing/stopBatchSendEmailTaskLogic.go @@ -0,0 +1,42 @@ +package marketing + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/task" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/email" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" +) + +type StopBatchSendEmailTaskLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewStopBatchSendEmailTaskLogic Stop a batch send email task +func NewStopBatchSendEmailTaskLogic(ctx context.Context, svcCtx *svc.ServiceContext) *StopBatchSendEmailTaskLogic { + return &StopBatchSendEmailTaskLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *StopBatchSendEmailTaskLogic) StopBatchSendEmailTask(req *types.StopBatchSendEmailTaskRequest) (err error) { + if email.Manager != nil { + email.Manager.RemoveWorker(req.Id) + } else { + logger.Error("[StopBatchSendEmailTaskLogic] email.Manager is nil, cannot stop task") + } + err = l.svcCtx.DB.Model(&task.Task{}).Where("id = ?", req.Id).Update("status", 2).Error + + if err != nil { + l.Errorf("failed to stop email task, error: %v", err) + return xerr.NewErrCode(xerr.DatabaseUpdateError) + } + return +} diff --git a/internal/logic/admin/payment/createPaymentMethodLogic.go b/internal/logic/admin/payment/createPaymentMethodLogic.go index 014f595..23cd48d 100644 --- a/internal/logic/admin/payment/createPaymentMethodLogic.go +++ b/internal/logic/admin/payment/createPaymentMethodLogic.go @@ -55,10 +55,9 @@ func (l *CreatePaymentMethodLogic) CreatePaymentMethod(req *types.CreatePaymentM Token: random.KeyNew(8, 1), } err = l.svcCtx.PaymentModel.Transaction(l.ctx, func(tx *gorm.DB) error { - if req.Platform == "Stripe" { var cfg paymentModel.StripeConfig - if err := cfg.Unmarshal(paymentMethod.Config); err != nil { + if err = cfg.Unmarshal([]byte(paymentMethod.Config)); err != nil { l.Errorf("[CreatePaymentMethod] unmarshal stripe config error: %s", err.Error()) return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "unmarshal stripe config error: %s", err.Error()) } @@ -79,7 +78,8 @@ func (l *CreatePaymentMethodLogic) CreatePaymentMethod(req *types.CreatePaymentM return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "create stripe webhook endpoint error: %s", err.Error()) } cfg.WebhookSecret = endpoint.Secret - paymentMethod.Config = cfg.Marshal() + content, _ := cfg.Marshal() + paymentMethod.Config = string(content) } if err = tx.Model(&paymentModel.Payment{}).Create(paymentMethod).Error; err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert payment method error: %s", err.Error()) @@ -101,27 +101,36 @@ func (l *CreatePaymentMethodLogic) CreatePaymentMethod(req *types.CreatePaymentM func parsePaymentPlatformConfig(ctx context.Context, platform payment.Platform, config interface{}) string { data, err := json.Marshal(config) if err != nil { - logger.WithContext(ctx).Errorw("parse payment platform config error", logger.Field("platform", platform), logger.Field("config", config), logger.Field("error", err.Error())) + logger.WithContext(ctx).Errorw("marshal config error", logger.Field("platform", platform), logger.Field("config", config), logger.Field("error", err.Error())) + return "" } + + // 通用处理函数 + handleConfig := func(name string, target interface { + Unmarshal([]byte) error + Marshal() ([]byte, error) + }) string { + if err = target.Unmarshal(data); err != nil { + logger.WithContext(ctx).Errorw("parse "+name+" config error", logger.Field("config", string(data)), logger.Field("error", err.Error())) + return "" + } + content, err := target.Marshal() + if err != nil { + logger.WithContext(ctx).Errorw("marshal "+name+" config error", logger.Field("error", err.Error())) + return "" + } + return string(content) + } + switch platform { case payment.Stripe: - stripe := &paymentModel.StripeConfig{} - if err := stripe.Unmarshal(string(data)); err != nil { - logger.WithContext(ctx).Errorw("parse stripe config error", logger.Field("config", string(data)), logger.Field("error", err.Error())) - } - return stripe.Marshal() + return handleConfig("Stripe", &paymentModel.StripeConfig{}) case payment.AlipayF2F: - alipay := &paymentModel.AlipayF2FConfig{} - if err := alipay.Unmarshal(string(data)); err != nil { - logger.WithContext(ctx).Errorw("parse alipay config error", logger.Field("config", string(data)), logger.Field("error", err.Error())) - } - return alipay.Marshal() + return handleConfig("Alipay", &paymentModel.AlipayF2FConfig{}) case payment.EPay: - epay := &paymentModel.EPayConfig{} - if err := epay.Unmarshal(string(data)); err != nil { - logger.WithContext(ctx).Errorw("parse epay config error", logger.Field("config", string(data)), logger.Field("error", err.Error())) - } - return epay.Marshal() + return handleConfig("Epay", &paymentModel.EPayConfig{}) + case payment.CryptoSaaS: + return handleConfig("CryptoSaaS", &paymentModel.CryptoSaaSConfig{}) default: return "" } diff --git a/internal/logic/admin/payment/getPaymentMethodListLogic.go b/internal/logic/admin/payment/getPaymentMethodListLogic.go index 6b610e7..ef987f3 100644 --- a/internal/logic/admin/payment/getPaymentMethodListLogic.go +++ b/internal/logic/admin/payment/getPaymentMethodListLogic.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" + "github.com/perfect-panel/server/internal/report" paymentPlatform "github.com/perfect-panel/server/pkg/payment" "github.com/perfect-panel/server/internal/model/payment" @@ -43,29 +44,46 @@ func (l *GetPaymentMethodListLogic) GetPaymentMethodList(req *types.GetPaymentMe Total: total, List: make([]types.PaymentMethodDetail, len(list)), } + + // gateway mod + + isGatewayMod := report.IsGatewayMode() + for i, v := range list { config := make(map[string]interface{}) _ = json.Unmarshal([]byte(v.Config), &config) notifyUrl := "" + if paymentPlatform.ParsePlatform(v.Platform) != paymentPlatform.Balance { + notifyUrl = v.Domain if v.Domain != "" { - notifyUrl = v.Domain + "/v1/notify/" + v.Platform + "/" + v.Token + // if is gateway mod, use gateway domain + if isGatewayMod { + notifyUrl += "/api/" + } + notifyUrl += "/v1/notify/" + v.Platform + "/" + v.Token } else { - notifyUrl = "https://" + l.svcCtx.Config.Host + "/v1/notify/" + v.Platform + "/" + v.Token + notifyUrl += "https://" + l.svcCtx.Config.Host + if isGatewayMod { + notifyUrl += "/api/v1/notify/" + v.Platform + "/" + v.Token + } else { + notifyUrl += "/v1/notify/" + v.Platform + "/" + v.Token + } } } resp.List[i] = types.PaymentMethodDetail{ - Id: v.Id, - Name: v.Name, - Platform: v.Platform, - Icon: v.Icon, - Domain: v.Domain, - Config: config, - FeeMode: v.FeeMode, - FeePercent: v.FeePercent, - FeeAmount: v.FeeAmount, - Enable: *v.Enable, - NotifyURL: notifyUrl, + Id: v.Id, + Name: v.Name, + Platform: v.Platform, + Icon: v.Icon, + Domain: v.Domain, + Config: config, + FeeMode: v.FeeMode, + FeePercent: v.FeePercent, + FeeAmount: v.FeeAmount, + Enable: *v.Enable, + NotifyURL: notifyUrl, + Description: v.Description, } } return diff --git a/internal/logic/admin/payment/updatePaymentMethodLogic.go b/internal/logic/admin/payment/updatePaymentMethodLogic.go index 87b4fd7..7c2dda2 100644 --- a/internal/logic/admin/payment/updatePaymentMethodLogic.go +++ b/internal/logic/admin/payment/updatePaymentMethodLogic.go @@ -19,7 +19,7 @@ type UpdatePaymentMethodLogic struct { svcCtx *svc.ServiceContext } -// Update Payment Method +// NewUpdatePaymentMethodLogic Update Payment Method func NewUpdatePaymentMethodLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdatePaymentMethodLogic { return &UpdatePaymentMethodLogic{ Logger: logger.WithContext(ctx), diff --git a/internal/logic/admin/server/batchDeleteNodeGroupLogic.go b/internal/logic/admin/server/batchDeleteNodeGroupLogic.go deleted file mode 100644 index 5353475..0000000 --- a/internal/logic/admin/server/batchDeleteNodeGroupLogic.go +++ /dev/null @@ -1,44 +0,0 @@ -package server - -import ( - "context" - - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" -) - -type BatchDeleteNodeGroupLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewBatchDeleteNodeGroupLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BatchDeleteNodeGroupLogic { - return &BatchDeleteNodeGroupLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *BatchDeleteNodeGroupLogic) BatchDeleteNodeGroup(req *types.BatchDeleteNodeGroupRequest) error { - // Check if the group is empty - count, err := l.svcCtx.ServerModel.QueryServerCountByServerGroups(l.ctx, req.Ids) - if err != nil { - l.Errorw("[BatchDeleteNodeGroup] Query Database Error: ", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query server error: %v", err) - } - if count > 0 { - return errors.Wrapf(xerr.NewErrCode(xerr.NodeGroupNotEmpty), "group is not empty") - } - // Delete the group - err = l.svcCtx.ServerModel.BatchDeleteNodeGroup(l.ctx, req.Ids) - if err != nil { - l.Errorw("[BatchDeleteNodeGroup] Delete Database Error: ", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), err.Error()) - } - return nil -} diff --git a/internal/logic/admin/server/batchDeleteNodeLogic.go b/internal/logic/admin/server/batchDeleteNodeLogic.go deleted file mode 100644 index 6b03806..0000000 --- a/internal/logic/admin/server/batchDeleteNodeLogic.go +++ /dev/null @@ -1,43 +0,0 @@ -package server - -import ( - "context" - - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" - "gorm.io/gorm" -) - -type BatchDeleteNodeLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewBatchDeleteNodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BatchDeleteNodeLogic { - return &BatchDeleteNodeLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *BatchDeleteNodeLogic) BatchDeleteNode(req *types.BatchDeleteNodeRequest) error { - err := l.svcCtx.DB.Transaction(func(db *gorm.DB) error { - for _, id := range req.Ids { - err := l.svcCtx.ServerModel.Delete(l.ctx, id) - if err != nil { - return err - } - } - return nil - }) - if err != nil { - l.Errorw("[BatchDeleteNode] Delete Database Error: ", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), err.Error()) - } - return nil -} diff --git a/internal/logic/admin/server/constant.go b/internal/logic/admin/server/constant.go new file mode 100644 index 0000000..ae06fea --- /dev/null +++ b/internal/logic/admin/server/constant.go @@ -0,0 +1,11 @@ +package server + +const ( + ShadowSocks = "shadowsocks" + Vmess = "vmess" + Vless = "vless" + Trojan = "trojan" + AnyTLS = "anytls" + Tuic = "tuic" + Hysteria2 = "hysteria2" +) diff --git a/internal/logic/admin/server/createNodeGroupLogic.go b/internal/logic/admin/server/createNodeGroupLogic.go deleted file mode 100644 index b194a24..0000000 --- a/internal/logic/admin/server/createNodeGroupLogic.go +++ /dev/null @@ -1,40 +0,0 @@ -package server - -import ( - "context" - - "github.com/perfect-panel/server/internal/model/server" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/xerr" - - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - - "github.com/pkg/errors" -) - -type CreateNodeGroupLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewCreateNodeGroupLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateNodeGroupLogic { - return &CreateNodeGroupLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *CreateNodeGroupLogic) CreateNodeGroup(req *types.CreateNodeGroupRequest) error { - groupInfo := &server.Group{ - Name: req.Name, - Description: req.Description, - } - err := l.svcCtx.ServerModel.InsertGroup(l.ctx, groupInfo) - if err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), err.Error()) - } - return nil -} diff --git a/internal/logic/admin/server/createNodeLogic.go b/internal/logic/admin/server/createNodeLogic.go index c25f9e1..f635f85 100644 --- a/internal/logic/admin/server/createNodeLogic.go +++ b/internal/logic/admin/server/createNodeLogic.go @@ -2,18 +2,13 @@ package server import ( "context" - "encoding/json" - "strings" - "time" - "github.com/hibiken/asynq" - "github.com/perfect-panel/server/internal/model/server" + "github.com/perfect-panel/server/internal/model/node" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/logger" "github.com/perfect-panel/server/pkg/tool" "github.com/perfect-panel/server/pkg/xerr" - queue "github.com/perfect-panel/server/queue/types" "github.com/pkg/errors" ) @@ -23,6 +18,7 @@ type CreateNodeLogic struct { svcCtx *svc.ServiceContext } +// NewCreateNodeLogic Create Node func NewCreateNodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateNodeLogic { return &CreateNodeLogic{ Logger: logger.WithContext(ctx), @@ -32,97 +28,18 @@ func NewCreateNodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Create } func (l *CreateNodeLogic) CreateNode(req *types.CreateNodeRequest) error { - config, err := json.Marshal(req.Config) - if err != nil { - return err + data := node.Node{ + Name: req.Name, + Tags: tool.StringSliceToString(req.Tags), + Port: req.Port, + Address: req.Address, + ServerId: req.ServerId, + Protocol: req.Protocol, } - var serverInfo server.Server - tool.DeepCopy(&serverInfo, req) - serverInfo.Config = string(config) - nodeRelay, err := json.Marshal(req.RelayNode) - if err != nil { - l.Errorw("[UpdateNode] Marshal RelayNode Error: ", logger.Field("error", err.Error())) - return err - } - if len(req.Tags) > 0 { - serverInfo.Tags = strings.Join(req.Tags, ",") - } - - serverInfo.LastReportedAt = time.UnixMicro(1218124800) - - serverInfo.City = req.City - serverInfo.Country = req.Country - - serverInfo.RelayNode = string(nodeRelay) - if req.Protocol == "vless" { - var cfg types.Vless - if err = json.Unmarshal(config, &cfg); err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "json.Unmarshal error: %v", err.Error()) - } - if cfg.Security == "reality" && cfg.SecurityConfig.RealityPublicKey == "" { - public, private, err := tool.Curve25519Genkey(false, "") - if err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "generate curve25519 key error") - } - cfg.SecurityConfig.RealityPublicKey = public - cfg.SecurityConfig.RealityPrivateKey = private - cfg.SecurityConfig.RealityShortId = tool.GenerateShortID(private) - } - if cfg.SecurityConfig.RealityServerAddr == "" { - cfg.SecurityConfig.RealityServerAddr = cfg.SecurityConfig.SNI - } - if cfg.SecurityConfig.RealityServerPort == 0 { - cfg.SecurityConfig.RealityServerPort = 443 - } - config, _ = json.Marshal(cfg) - serverInfo.Config = string(config) - } else if req.Protocol == "shadowsocks" { - var cfg types.Shadowsocks - if err = json.Unmarshal(config, &cfg); err != nil { - l.Errorf("[CreateNode] Unmarshal Shadowsocks Config Error: %v", err.Error()) - return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "json.Unmarshal error: %v", err.Error()) - } - if strings.Contains(cfg.Method, "2022") { - var length int - switch cfg.Method { - case "2022-blake3-aes-128-gcm": - length = 16 - default: - length = 32 - } - if len(cfg.ServerKey) != length { - cfg.ServerKey = tool.GenerateCipher(cfg.ServerKey, length) - } - } - config, _ = json.Marshal(cfg) - serverInfo.Config = string(config) - } - - err = l.svcCtx.ServerModel.Insert(l.ctx, &serverInfo) + err := l.svcCtx.NodeModel.InsertNode(l.ctx, &data) if err != nil { l.Errorw("[CreateNode] Insert Database Error: ", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create server error: %v", err) - } - - if req.City == "" || req.Country == "" { - // Marshal the task payload - payload, err := json.Marshal(queue.GetNodeCountry{ - Protocol: serverInfo.Protocol, - ServerAddr: serverInfo.ServerAddr, - }) - if err != nil { - l.Errorw("[GetNodeCountry]: Marshal Error", logger.Field("error", err.Error())) - return errors.Wrap(xerr.NewErrCode(xerr.ERROR), "Failed to marshal task payload") - } - // Create a queue task - task := asynq.NewTask(queue.ForthwithGetCountry, payload) - // Enqueue the task - taskInfo, err := l.svcCtx.Queue.Enqueue(task) - if err != nil { - l.Errorw("[GetNodeCountry]: Enqueue Error", logger.Field("error", err.Error()), logger.Field("payload", string(payload))) - return errors.Wrap(xerr.NewErrCode(xerr.ERROR), "Failed to enqueue task") - } - l.Infow("[GetNodeCountry]: Enqueue Success", logger.Field("taskID", taskInfo.ID), logger.Field("payload", string(payload))) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "[CreateNode] Insert Database Error") } return nil diff --git a/internal/logic/admin/server/createRuleGroupLogic.go b/internal/logic/admin/server/createRuleGroupLogic.go deleted file mode 100644 index 619fbe7..0000000 --- a/internal/logic/admin/server/createRuleGroupLogic.go +++ /dev/null @@ -1,78 +0,0 @@ -package server - -import ( - "context" - "strings" - - "github.com/perfect-panel/server/pkg/rules" - - "github.com/perfect-panel/server/internal/model/server" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/tool" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" -) - -type CreateRuleGroupLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Create rule group -func NewCreateRuleGroupLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateRuleGroupLogic { - return &CreateRuleGroupLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} -func parseAndValidateRules(ruleText, ruleName string) ([]string, error) { - var rs []string - ruleArr := strings.Split(ruleText, "\n") - if len(ruleArr) == 0 { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "rules is empty") - } - - for _, s := range ruleArr { - r := rules.NewRule(s, ruleName) - if r == nil { - continue - } - if err := r.Validate(); err != nil { - continue - } - rs = append(rs, r.String()) - } - return rs, nil -} -func (l *CreateRuleGroupLogic) CreateRuleGroup(req *types.CreateRuleGroupRequest) error { - rs, err := parseAndValidateRules(req.Rules, req.Name) - if err != nil { - return err - } - info := &server.RuleGroup{ - Name: req.Name, - Icon: req.Icon, - Type: req.Type, - Tags: tool.StringSliceToString(req.Tags), - Rules: strings.Join(rs, "\n"), - Default: req.Default, - Enable: req.Enable, - } - err = l.svcCtx.ServerModel.InsertRuleGroup(l.ctx, info) - if err != nil { - l.Errorw("[CreateRuleGroup] Insert Database Error: ", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create server rule group error: %v", err) - } - if req.Default { - if err = l.svcCtx.ServerModel.SetDefaultRuleGroup(l.ctx, info.Id); err != nil { - l.Errorw("[CreateRuleGroup] Set Default Rule Group Error: ", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "set default rule group error: %v", err) - } - } - - return nil -} diff --git a/internal/logic/admin/server/createServerLogic.go b/internal/logic/admin/server/createServerLogic.go new file mode 100644 index 0000000..2ab1993 --- /dev/null +++ b/internal/logic/admin/server/createServerLogic.go @@ -0,0 +1,110 @@ +package server + +import ( + "context" + "strings" + + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/ip" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type CreateServerLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewCreateServerLogic Create Server +func NewCreateServerLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateServerLogic { + return &CreateServerLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CreateServerLogic) CreateServer(req *types.CreateServerRequest) error { + data := node.Server{ + Name: req.Name, + Country: req.Country, + City: req.City, + Address: req.Address, + Sort: req.Sort, + Protocols: "", + } + protocols := make([]node.Protocol, 0) + for _, item := range req.Protocols { + if item.Type == "" { + return errors.Wrapf(xerr.NewErrCodeMsg(xerr.InvalidParams, "protocols type is empty"), "protocols type is empty") + } + var protocol node.Protocol + tool.DeepCopy(&protocol, item) + + // VLESS Reality Key Generation + if protocol.Type == "vless" { + if protocol.Security == "reality" { + if protocol.RealityPublicKey == "" { + public, private, err := tool.Curve25519Genkey(false, "") + if err != nil { + l.Errorf("[CreateServer] Generate Reality Key Error: %v", err.Error()) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "generate reality key error: %v", err) + } + protocol.RealityPublicKey = public + protocol.RealityPrivateKey = private + protocol.RealityShortId = tool.GenerateShortID(private) + } + if protocol.RealityServerAddr == "" { + protocol.RealityServerAddr = protocol.SNI + } + if protocol.RealityServerPort == 0 { + protocol.RealityServerPort = 443 + } + } + + } + // ShadowSocks 2022 Key Generation + if protocol.Type == "shadowsocks" { + if strings.Contains(protocol.Cipher, "2022") { + var length int + switch protocol.Cipher { + case "2022-blake3-aes-128-gcm": + length = 16 + default: + length = 32 + } + if len(protocol.ServerKey) != length { + protocol.ServerKey = tool.GenerateCipher(protocol.ServerKey, length) + } + } + } + protocols = append(protocols, protocol) + } + + err := data.MarshalProtocols(protocols) + if err != nil { + l.Errorf("[CreateServer] Marshal Protocols Error: %v", err.Error()) + return errors.Wrapf(xerr.NewErrCodeMsg(xerr.InvalidParams, "protocols marshal error"), "protocols marshal error: %v", err) + } + if data.City == "" && data.Country == "" { + // query server ip location + result, err := ip.GetRegionByIp(req.Address) + if err != nil { + l.Errorf("[CreateServer] GetRegionByIp Error: %v", err.Error()) + } else { + data.City = result.City + data.Country = result.Country + } + } + err = l.svcCtx.NodeModel.InsertServer(l.ctx, &data) + if err != nil { + l.Errorf("[CreateServer] Insert Server error: %v", err.Error()) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert server error: %v", err) + } + return nil +} diff --git a/internal/logic/admin/server/deleteNodeGroupLogic.go b/internal/logic/admin/server/deleteNodeGroupLogic.go deleted file mode 100644 index 04d420e..0000000 --- a/internal/logic/admin/server/deleteNodeGroupLogic.go +++ /dev/null @@ -1,44 +0,0 @@ -package server - -import ( - "context" - - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" -) - -type DeleteNodeGroupLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewDeleteNodeGroupLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteNodeGroupLogic { - return &DeleteNodeGroupLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *DeleteNodeGroupLogic) DeleteNodeGroup(req *types.DeleteNodeGroupRequest) error { - // Check if the group is empty - count, err := l.svcCtx.ServerModel.QueryServerCountByServerGroups(l.ctx, []int64{req.Id}) - if err != nil { - l.Errorw("[DeleteNodeGroup] Query Database Error: ", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query server error: %v", err) - } - if count > 0 { - return errors.Wrapf(xerr.NewErrCode(xerr.NodeGroupNotEmpty), "group is not empty") - } - // Delete the group - err = l.svcCtx.ServerModel.DeleteGroup(l.ctx, req.Id) - if err != nil { - l.Errorw("[DeleteNodeGroup] Delete Database Error: ", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), err.Error()) - } - return nil -} diff --git a/internal/logic/admin/server/deleteNodeLogic.go b/internal/logic/admin/server/deleteNodeLogic.go index cd4e4c6..8a5f6a0 100644 --- a/internal/logic/admin/server/deleteNodeLogic.go +++ b/internal/logic/admin/server/deleteNodeLogic.go @@ -2,14 +2,14 @@ package server import ( "context" - "github.com/perfect-panel/server/pkg/tool" + "strings" + "github.com/perfect-panel/server/internal/model/node" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/logger" "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" - "gorm.io/gorm" ) type DeleteNodeLogic struct { @@ -18,6 +18,7 @@ type DeleteNodeLogic struct { svcCtx *svc.ServiceContext } +// NewDeleteNodeLogic Delete Node func NewDeleteNodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteNodeLogic { return &DeleteNodeLogic{ Logger: logger.WithContext(ctx), @@ -27,33 +28,20 @@ func NewDeleteNodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Delete } func (l *DeleteNodeLogic) DeleteNode(req *types.DeleteNodeRequest) error { - err := l.svcCtx.DB.Transaction(func(tx *gorm.DB) error { - // Delete server - err := l.svcCtx.ServerModel.Delete(l.ctx, req.Id, tx) - if err != nil { - return err - } - // Delete server to subscribe - subs, err := l.svcCtx.SubscribeModel.QuerySubscribeIdsByServerIdAndServerGroupId(l.ctx, req.Id, 0) - if err != nil { - l.Logger.Errorf("[DeleteNode] QuerySubscribeIdsByServerIdAndServerGroupId error: %v", err.Error()) - return err - } + data, err := l.svcCtx.NodeModel.FindOneNode(l.ctx, req.Id) - for _, sub := range subs { - servers := tool.StringToInt64Slice(sub.Server) - newServers := tool.RemoveElementBySlice(servers, req.Id) - sub.Server = tool.Int64SliceToString(newServers) - if err = l.svcCtx.SubscribeModel.Update(l.ctx, sub, tx); err != nil { - l.Logger.Errorf("[DeleteNode] UpdateSubscribe error: %v", err.Error()) - return err - } - } - return nil - }) + err = l.svcCtx.NodeModel.DeleteNode(l.ctx, req.Id) if err != nil { l.Errorw("[DeleteNode] Delete Database Error: ", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete server error: %v", err) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "[DeleteNode] Delete Database Error") } - return nil + + return l.svcCtx.NodeModel.ClearNodeCache(l.ctx, &node.FilterNodeParams{ + Page: 1, + Size: 1000, + ServerId: []int64{data.ServerId}, + Tag: strings.Split(data.Tags, ","), + Search: "", + Protocol: data.Protocol, + }) } diff --git a/internal/logic/admin/server/deleteRuleGroupLogic.go b/internal/logic/admin/server/deleteRuleGroupLogic.go deleted file mode 100644 index f0e1110..0000000 --- a/internal/logic/admin/server/deleteRuleGroupLogic.go +++ /dev/null @@ -1,35 +0,0 @@ -package server - -import ( - "context" - - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" -) - -type DeleteRuleGroupLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Delete rule group -func NewDeleteRuleGroupLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteRuleGroupLogic { - return &DeleteRuleGroupLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *DeleteRuleGroupLogic) DeleteRuleGroup(req *types.DeleteRuleGroupRequest) error { - err := l.svcCtx.ServerModel.DeleteRuleGroup(l.ctx, req.Id) - if err != nil { - l.Errorw("[DeleteRuleGroup] Delete Database Error: ", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete server rule group error: %v", err) - } - return nil -} diff --git a/internal/logic/admin/server/deleteServerLogic.go b/internal/logic/admin/server/deleteServerLogic.go new file mode 100644 index 0000000..186d14a --- /dev/null +++ b/internal/logic/admin/server/deleteServerLogic.go @@ -0,0 +1,41 @@ +package server + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type DeleteServerLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewDeleteServerLogic Delete Server +func NewDeleteServerLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteServerLogic { + return &DeleteServerLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *DeleteServerLogic) DeleteServer(req *types.DeleteServerRequest) error { + err := l.svcCtx.NodeModel.DeleteServer(l.ctx, req.Id) + if err != nil { + l.Errorw("[DeleteServer] Delete Server Error: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "[DeleteServer] Delete Server Error") + } + return l.svcCtx.NodeModel.ClearNodeCache(l.ctx, &node.FilterNodeParams{ + Page: 1, + Size: 1000, + ServerId: []int64{req.Id}, + Search: "", + }) +} diff --git a/internal/logic/admin/server/filterNodeListLogic.go b/internal/logic/admin/server/filterNodeListLogic.go new file mode 100644 index 0000000..2e41cec --- /dev/null +++ b/internal/logic/admin/server/filterNodeListLogic.go @@ -0,0 +1,64 @@ +package server + +import ( + "context" + "strings" + + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type FilterNodeListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewFilterNodeListLogic Filter Node List +func NewFilterNodeListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FilterNodeListLogic { + return &FilterNodeListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *FilterNodeListLogic) FilterNodeList(req *types.FilterNodeListRequest) (resp *types.FilterNodeListResponse, err error) { + total, data, err := l.svcCtx.NodeModel.FilterNodeList(l.ctx, &node.FilterNodeParams{ + Page: req.Page, + Size: req.Size, + Search: req.Search, + }) + + if err != nil { + l.Errorw("[FilterNodeList] Query Database Error: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "[FilterNodeList] Query Database Error") + } + + list := make([]types.Node, 0) + for _, datum := range data { + list = append(list, types.Node{ + Id: datum.Id, + Name: datum.Name, + Tags: tool.RemoveDuplicateElements(strings.Split(datum.Tags, ",")...), + Port: datum.Port, + Address: datum.Address, + ServerId: datum.ServerId, + Protocol: datum.Protocol, + Enabled: datum.Enabled, + Sort: datum.Sort, + CreatedAt: datum.CreatedAt.UnixMilli(), + UpdatedAt: datum.UpdatedAt.UnixMilli(), + }) + } + + return &types.FilterNodeListResponse{ + List: list, + Total: total, + }, nil +} diff --git a/internal/logic/admin/server/filterServerListLogic.go b/internal/logic/admin/server/filterServerListLogic.go new file mode 100644 index 0000000..fb47ffa --- /dev/null +++ b/internal/logic/admin/server/filterServerListLogic.go @@ -0,0 +1,164 @@ +package server + +import ( + "context" + "time" + + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +type FilterServerListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewFilterServerListLogic Filter Server List +func NewFilterServerListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FilterServerListLogic { + return &FilterServerListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *FilterServerListLogic) FilterServerList(req *types.FilterServerListRequest) (resp *types.FilterServerListResponse, err error) { + total, data, err := l.svcCtx.NodeModel.FilterServerList(l.ctx, &node.FilterParams{ + Page: req.Page, + Size: req.Size, + Search: req.Search, + }) + if err != nil { + l.Errorw("[FilterServerList] Query Database Error: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "[FilterServerList] Query Database Error") + } + + list := make([]types.Server, 0) + + for _, datum := range data { + var server types.Server + tool.DeepCopy(&server, datum) + + // handler protocols + var protocols []types.Protocol + dst, err := datum.UnmarshalProtocols() + if err != nil { + l.Errorf("[FilterServerList] UnmarshalProtocols Error: %s", err.Error()) + continue + } + tool.DeepCopy(&protocols, dst) + server.Protocols = protocols + + nodeStatus, err := l.svcCtx.NodeModel.StatusCache(l.ctx, datum.Id) + if err != nil { + if !errors.Is(err, redis.Nil) { + l.Errorw("[handlerServerStatus] GetNodeStatus Error: ", logger.Field("error", err.Error()), logger.Field("node_id", datum.Id)) + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetNodeStatus Error") + } + server.Status = types.ServerStatus{ + Mem: nodeStatus.Mem, + Cpu: nodeStatus.Cpu, + Disk: nodeStatus.Disk, + Online: l.handlerServerStatus(datum.Id, protocols), + Status: l.handlerServerStaus(datum.LastReportedAt), + } + list = append(list, server) + } + + return &types.FilterServerListResponse{ + List: list, + Total: total, + }, nil +} + +func (l *FilterServerListLogic) handlerServerStatus(id int64, protocols []types.Protocol) []types.ServerOnlineUser { + result := make([]types.ServerOnlineUser, 0) + + for _, protocol := range protocols { + // query online user + data, err := l.svcCtx.NodeModel.OnlineUserSubscribe(l.ctx, id, protocol.Type) + if err != nil { + if !errors.Is(err, redis.Nil) { + l.Errorw("[handlerServerStatus] OnlineUserSubscribe Error: ", logger.Field("error", err.Error()), logger.Field("node_id", id), logger.Field("protocol", protocol.Type)) + } + continue + } + if len(data) > 0 { + for sub, online := range data { + var ips []types.ServerOnlineIP + for _, ip := range online { + ips = append(ips, types.ServerOnlineIP{ + IP: ip, + Protocol: protocol.Type, + }) + } + + result = append(result, types.ServerOnlineUser{ + IP: ips, + SubscribeId: sub, + }) + } + } + } + // merge same subscribe + var mapResult = make(map[int64]types.ServerOnlineUser) + for _, item := range result { + if exist, ok := mapResult[item.SubscribeId]; ok { + // merge + exist.Traffic += item.Traffic + exist.IP = append(exist.IP, item.IP...) + mapResult[item.SubscribeId] = exist + } else { + // get subscribe info + info, err := l.svcCtx.UserModel.FindOneUserSubscribe(l.ctx, item.SubscribeId) + if err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + l.Errorw("[handlerServerStatus] FindOneSubscribe Error: ", logger.Field("error", err.Error()), logger.Field("subscribe_id", item.SubscribeId)) + } + continue + } + data := types.ServerOnlineUser{ + IP: item.IP, + UserId: info.UserId, + Subscribe: "", + SubscribeId: item.SubscribeId, + Traffic: info.Download + info.Upload, + ExpiredAt: info.ExpireTime.UnixMilli(), + } + if info.Subscribe != nil { + data.Subscribe = info.Subscribe.Name + } + // add new + mapResult[item.SubscribeId] = data + } + } + // convert map to slice + result = make([]types.ServerOnlineUser, 0, len(mapResult)) + for _, item := range mapResult { + result = append(result, item) + } + return result +} + +func (l *FilterServerListLogic) handlerServerStaus(last *time.Time) string { + if last == nil { + return "offline" + } + if time.Since(*last) > time.Minute*5 { + return "offline" + } + if time.Since(*last) > time.Minute*3 { + return "warning" + } + return "online" + +} diff --git a/internal/logic/admin/server/getNodeDetailLogic.go b/internal/logic/admin/server/getNodeDetailLogic.go deleted file mode 100644 index 2feb8fe..0000000 --- a/internal/logic/admin/server/getNodeDetailLogic.go +++ /dev/null @@ -1,43 +0,0 @@ -package server - -import ( - "context" - "encoding/json" - - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/tool" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" -) - -type GetNodeDetailLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewGetNodeDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetNodeDetailLogic { - return &GetNodeDetailLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *GetNodeDetailLogic) GetNodeDetail(req *types.GetDetailRequest) (resp *types.Server, err error) { - detail, err := l.svcCtx.ServerModel.FindOne(l.ctx, req.Id) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get server detail error: %v", err.Error()) - } - resp = &types.Server{} - tool.DeepCopy(resp, detail) - var cfg map[string]interface{} - err = json.Unmarshal([]byte(detail.Config), &cfg) - if err != nil { - cfg = make(map[string]interface{}) - } - resp.Config = cfg - return -} diff --git a/internal/logic/admin/server/getNodeGroupListLogic.go b/internal/logic/admin/server/getNodeGroupListLogic.go deleted file mode 100644 index 6ab53c8..0000000 --- a/internal/logic/admin/server/getNodeGroupListLogic.go +++ /dev/null @@ -1,39 +0,0 @@ -package server - -import ( - "context" - - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/tool" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" -) - -type GetNodeGroupListLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewGetNodeGroupListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetNodeGroupListLogic { - return &GetNodeGroupListLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *GetNodeGroupListLogic) GetNodeGroupList() (resp *types.GetNodeGroupListResponse, err error) { - nodeGroupList, err := l.svcCtx.ServerModel.QueryAllGroup(l.ctx) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), err.Error()) - } - nodeGroups := make([]types.ServerGroup, 0) - tool.DeepCopy(&nodeGroups, nodeGroupList) - return &types.GetNodeGroupListResponse{ - Total: int64(len(nodeGroups)), - List: nodeGroups, - }, nil -} diff --git a/internal/logic/admin/server/getNodeListLogic.go b/internal/logic/admin/server/getNodeListLogic.go deleted file mode 100644 index 544c85d..0000000 --- a/internal/logic/admin/server/getNodeListLogic.go +++ /dev/null @@ -1,104 +0,0 @@ -package server - -import ( - "context" - "encoding/json" - "strings" - - "github.com/perfect-panel/server/internal/model/server" - - "github.com/redis/go-redis/v9" - - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/tool" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" -) - -type GetNodeListLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewGetNodeListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetNodeListLogic { - return &GetNodeListLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *GetNodeListLogic) GetNodeList(req *types.GetNodeServerListRequest) (resp *types.GetNodeServerListResponse, err error) { - tags := make([]string, 0) - if req.Tags != "" { - tags = strings.Split(req.Tags, ",") - } - total, list, err := l.svcCtx.ServerModel.FindServerListByFilter(l.ctx, &server.ServerFilter{ - Page: req.Page, - Size: req.Size, - Search: req.Search, - Tags: tags, - Group: req.GroupId, - }) - if err != nil { - l.Errorw("[GetNodeList] Query Database Error: ", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), err.Error()) - } - nodes := make([]types.Server, 0) - for _, v := range list { - node := types.Server{} - tool.DeepCopy(&node, v) - // default relay mode - if node.RelayMode == "" { - node.RelayMode = "none" - } - if len(v.Tags) > 0 { - if strings.Contains(v.Tags, ",") { - node.Tags = strings.Split(v.Tags, ",") - } else { - node.Tags = []string{v.Tags} - } - } - // parse config - var cfg map[string]interface{} - err = json.Unmarshal([]byte(v.Config), &cfg) - if err != nil { - cfg = make(map[string]interface{}) - } - node.Config = cfg - relayNode := make([]types.NodeRelay, 0) - err = json.Unmarshal([]byte(v.RelayNode), &relayNode) - if err != nil { - l.Errorw("[GetNodeList] Unmarshal RelayNode Error: ", logger.Field("error", err.Error()), logger.Field("relayNode", v.RelayNode)) - } - node.RelayNode = relayNode - var status types.NodeStatus - nodeStatus, err := l.svcCtx.NodeCache.GetNodeStatus(l.ctx, v.Id) - if err != nil { - // redis nil is not a Error - if !errors.Is(err, redis.Nil) { - l.Errorw("[GetNodeList] Get Node Status Error: ", logger.Field("error", err.Error())) - } - } else { - onlineUser, err := l.svcCtx.NodeCache.GetNodeOnlineUser(l.ctx, v.Id) - if err != nil { - l.Errorw("[GetNodeList] Get Node Online User Error: ", logger.Field("error", err.Error())) - } else { - status.Online = onlineUser - } - status.Cpu = nodeStatus.Cpu - status.Mem = nodeStatus.Mem - status.Disk = nodeStatus.Disk - status.UpdatedAt = nodeStatus.UpdatedAt - } - node.Status = &status - nodes = append(nodes, node) - } - return &types.GetNodeServerListResponse{ - Total: total, - List: nodes, - }, nil -} diff --git a/internal/logic/admin/server/getNodeTagListLogic.go b/internal/logic/admin/server/getNodeTagListLogic.go deleted file mode 100644 index 36eb9e7..0000000 --- a/internal/logic/admin/server/getNodeTagListLogic.go +++ /dev/null @@ -1,31 +0,0 @@ -package server - -import ( - "context" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/tool" -) - -type GetNodeTagListLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Get node tag list -func NewGetNodeTagListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetNodeTagListLogic { - return &GetNodeTagListLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *GetNodeTagListLogic) GetNodeTagList() (resp *types.GetNodeTagListResponse, err error) { - tags, err := l.svcCtx.ServerModel.FindServerTags(l.ctx) - return &types.GetNodeTagListResponse{ - Tags: tool.RemoveDuplicateElements(tags...), - }, nil -} diff --git a/internal/logic/admin/server/getRuleGroupListLogic.go b/internal/logic/admin/server/getRuleGroupListLogic.go deleted file mode 100644 index c3e1a3f..0000000 --- a/internal/logic/admin/server/getRuleGroupListLogic.go +++ /dev/null @@ -1,54 +0,0 @@ -package server - -import ( - "context" - "strings" - - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" -) - -type GetRuleGroupListLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Get rule group list -func NewGetRuleGroupListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetRuleGroupListLogic { - return &GetRuleGroupListLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *GetRuleGroupListLogic) GetRuleGroupList() (resp *types.GetRuleGroupResponse, err error) { - nodeRuleGroupList, err := l.svcCtx.ServerModel.QueryAllRuleGroup(l.ctx) - if err != nil { - l.Errorw("[GetRuleGroupList] Query Database Error: ", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), err.Error()) - } - nodeRuleGroups := make([]types.ServerRuleGroup, len(nodeRuleGroupList)) - for i, v := range nodeRuleGroupList { - nodeRuleGroups[i] = types.ServerRuleGroup{ - Id: v.Id, - Icon: v.Icon, - Name: v.Name, - Type: v.Type, - Tags: strings.Split(v.Tags, ","), - Rules: v.Rules, - Enable: v.Enable, - Default: v.Default, - CreatedAt: v.CreatedAt.UnixMilli(), - UpdatedAt: v.UpdatedAt.UnixMilli(), - } - } - return &types.GetRuleGroupResponse{ - Total: int64(len(nodeRuleGroups)), - List: nodeRuleGroups, - }, nil -} diff --git a/internal/logic/admin/server/getServerProtocolsLogic.go b/internal/logic/admin/server/getServerProtocolsLogic.go new file mode 100644 index 0000000..66519d9 --- /dev/null +++ b/internal/logic/admin/server/getServerProtocolsLogic.go @@ -0,0 +1,49 @@ +package server + +import ( + "context" + + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetServerProtocolsLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get Server Protocols +func NewGetServerProtocolsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetServerProtocolsLogic { + return &GetServerProtocolsLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetServerProtocolsLogic) GetServerProtocols(req *types.GetServerProtocolsRequest) (resp *types.GetServerProtocolsResponse, err error) { + // find server + data, err := l.svcCtx.NodeModel.FindOneServer(l.ctx, req.Id) + if err != nil { + l.Errorf("[GetServerProtocols] FindOneServer Error: %s", err.Error()) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "[GetServerProtocols] FindOneServer Error: %s", err.Error()) + } + + // handler protocols + var protocols []types.Protocol + dst, err := data.UnmarshalProtocols() + if err != nil { + l.Errorf("[FilterServerList] UnmarshalProtocols Error: %s", err.Error()) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "[FilterServerList] UnmarshalProtocols Error: %s", err.Error()) + } + tool.DeepCopy(&protocols, dst) + + return &types.GetServerProtocolsResponse{ + Protocols: protocols, + }, nil +} diff --git a/internal/logic/admin/server/hasMigrateSeverNodeLogic.go b/internal/logic/admin/server/hasMigrateSeverNodeLogic.go new file mode 100644 index 0000000..128b7f6 --- /dev/null +++ b/internal/logic/admin/server/hasMigrateSeverNodeLogic.go @@ -0,0 +1,52 @@ +package server + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/internal/model/server" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type HasMigrateSeverNodeLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewHasMigrateSeverNodeLogic Check if there is any server or node to migrate +func NewHasMigrateSeverNodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *HasMigrateSeverNodeLogic { + return &HasMigrateSeverNodeLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *HasMigrateSeverNodeLogic) HasMigrateSeverNode() (resp *types.HasMigrateSeverNodeResponse, err error) { + var oldCount, newCount int64 + query := l.svcCtx.DB.WithContext(l.ctx) + + err = query.Model(&server.Server{}).Count(&oldCount).Error + if err != nil { + l.Errorw("[HasMigrateSeverNode] Query Old Server Count Error: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "[HasMigrateSeverNode] Query Old Server Count Error") + } + err = query.Model(&node.Server{}).Count(&newCount).Error + if err != nil { + l.Errorw("[HasMigrateSeverNode] Query New Server Count Error: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "[HasMigrateSeverNode] Query New Server Count Error") + } + var shouldMigrate bool + if oldCount != 0 && newCount == 0 { + shouldMigrate = true + } + + return &types.HasMigrateSeverNodeResponse{ + HasMigrate: shouldMigrate, + }, nil +} diff --git a/internal/logic/admin/server/migrateServerNodeLogic.go b/internal/logic/admin/server/migrateServerNodeLogic.go new file mode 100644 index 0000000..4f0a497 --- /dev/null +++ b/internal/logic/admin/server/migrateServerNodeLogic.go @@ -0,0 +1,338 @@ +package server + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/internal/model/server" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" +) + +type MigrateServerNodeLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewMigrateServerNodeLogic Migrate server and node data to new database +func NewMigrateServerNodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *MigrateServerNodeLogic { + return &MigrateServerNodeLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *MigrateServerNodeLogic) MigrateServerNode() (resp *types.MigrateServerNodeResponse, err error) { + tx := l.svcCtx.DB.WithContext(l.ctx).Begin() + var oldServers []*server.Server + var newServers []*node.Server + var newNodes []*node.Node + + err = tx.Model(&server.Server{}).Find(&oldServers).Error + if err != nil { + l.Errorw("[MigrateServerNode] Query Old Server List Error: ", logger.Field("error", err.Error())) + return &types.MigrateServerNodeResponse{ + Succee: 0, + Fail: 0, + Message: fmt.Sprintf("Query Old Server List Error: %s", err.Error()), + }, nil + } + for _, oldServer := range oldServers { + data, err := l.adapterServer(oldServer) + if err != nil { + l.Errorw("[MigrateServerNode] Adapter Server Error: ", logger.Field("error", err.Error())) + if resp == nil { + resp = &types.MigrateServerNodeResponse{} + } + resp.Fail++ + if resp.Message == "" { + resp.Message = fmt.Sprintf("Adapter Server Error: %s", err.Error()) + } else { + resp.Message = fmt.Sprintf("%s; Adapter Server Error: %s", resp.Message, err.Error()) + } + continue + } + newServers = append(newServers, data) + + newNode, err := l.adapterNode(oldServer) + if err != nil { + l.Errorw("[MigrateServerNode] Adapter Node Error: ", logger.Field("error", err.Error())) + if resp == nil { + resp = &types.MigrateServerNodeResponse{} + } + resp.Fail++ + if resp.Message == "" { + resp.Message = fmt.Sprintf("Adapter Node Error: %s", err.Error()) + } else { + resp.Message = fmt.Sprintf("%s; Adapter Node Error: %s", resp.Message, err.Error()) + } + continue + } + for _, item := range newNode { + if item.Port == 0 { + protocols, _ := data.UnmarshalProtocols() + if len(protocols) > 0 { + item.Port = protocols[0].Port + } + } + newNodes = append(newNodes, item) + } + } + + if len(newServers) > 0 { + err = tx.Model(&node.Server{}).CreateInBatches(newServers, 20).Error + if err != nil { + tx.Rollback() + l.Errorw("[MigrateServerNode] Insert New Server List Error: ", logger.Field("error", err.Error())) + return &types.MigrateServerNodeResponse{ + Succee: 0, + Fail: uint64(len(newServers)), + Message: fmt.Sprintf("Insert New Server List Error: %s", err.Error()), + }, nil + } + } + if len(newNodes) > 0 { + err = tx.Model(&node.Node{}).CreateInBatches(newNodes, 20).Error + if err != nil { + tx.Rollback() + l.Errorw("[MigrateServerNode] Insert New Node List Error: ", logger.Field("error", err.Error())) + return &types.MigrateServerNodeResponse{ + Succee: uint64(len(newServers)), + Fail: uint64(len(newNodes)), + Message: fmt.Sprintf("Insert New Node List Error: %s", err.Error()), + }, nil + } + } + tx.Commit() + + return &types.MigrateServerNodeResponse{ + Succee: uint64(len(newServers)), + Fail: 0, + Message: fmt.Sprintf("Migrate Success: %d servers and %d nodes", len(newServers), len(newNodes)), + }, nil +} + +func (l *MigrateServerNodeLogic) adapterServer(info *server.Server) (*node.Server, error) { + result := &node.Server{ + Id: info.Id, + Name: info.Name, + Country: info.Country, + City: info.City, + //Ratio: info.TrafficRatio, + Address: info.ServerAddr, + Sort: int(info.Sort), + Protocols: "", + } + var protocols []node.Protocol + + switch info.Protocol { + case ShadowSocks: + var src server.Shadowsocks + err := json.Unmarshal([]byte(info.Config), &src) + if err != nil { + return nil, err + } + protocols = append(protocols, node.Protocol{ + Type: "shadowsocks", + Cipher: src.Method, + Port: uint16(src.Port), + ServerKey: src.ServerKey, + Ratio: float64(info.TrafficRatio), + }) + case Vmess: + var src server.Vmess + err := json.Unmarshal([]byte(info.Config), &src) + if err != nil { + return nil, err + } + protocol := node.Protocol{ + Type: "vmess", + Port: uint16(src.Port), + Security: src.Security, + SNI: src.SecurityConfig.SNI, + AllowInsecure: src.SecurityConfig.AllowInsecure, + Fingerprint: src.SecurityConfig.Fingerprint, + RealityServerAddr: src.SecurityConfig.RealityServerAddr, + RealityServerPort: src.SecurityConfig.RealityServerPort, + RealityPrivateKey: src.SecurityConfig.RealityPrivateKey, + RealityPublicKey: src.SecurityConfig.RealityPublicKey, + RealityShortId: src.SecurityConfig.RealityShortId, + Transport: src.Transport, + Host: src.TransportConfig.Host, + Path: src.TransportConfig.Path, + ServiceName: src.TransportConfig.ServiceName, + Flow: src.Flow, + Ratio: float64(info.TrafficRatio), + } + protocols = append(protocols, protocol) + protocols = append(protocols, protocol) + case Vless: + var src server.Vless + err := json.Unmarshal([]byte(info.Config), &src) + if err != nil { + return nil, err + } + protocol := node.Protocol{ + Type: "vless", + Port: uint16(src.Port), + Security: src.Security, + SNI: src.SecurityConfig.SNI, + AllowInsecure: src.SecurityConfig.AllowInsecure, + Fingerprint: src.SecurityConfig.Fingerprint, + RealityServerAddr: src.SecurityConfig.RealityServerAddr, + RealityServerPort: src.SecurityConfig.RealityServerPort, + RealityPrivateKey: src.SecurityConfig.RealityPrivateKey, + RealityPublicKey: src.SecurityConfig.RealityPublicKey, + RealityShortId: src.SecurityConfig.RealityShortId, + Transport: src.Transport, + Host: src.TransportConfig.Host, + Path: src.TransportConfig.Path, + ServiceName: src.TransportConfig.ServiceName, + Flow: src.Flow, + Ratio: float64(info.TrafficRatio), + } + protocols = append(protocols, protocol) + case Trojan: + var src server.Trojan + err := json.Unmarshal([]byte(info.Config), &src) + if err != nil { + return nil, err + } + protocol := node.Protocol{ + Type: "trojan", + Port: uint16(src.Port), + Security: src.Security, + SNI: src.SecurityConfig.SNI, + AllowInsecure: src.SecurityConfig.AllowInsecure, + Fingerprint: src.SecurityConfig.Fingerprint, + RealityServerAddr: src.SecurityConfig.RealityServerAddr, + RealityServerPort: src.SecurityConfig.RealityServerPort, + RealityPrivateKey: src.SecurityConfig.RealityPrivateKey, + RealityPublicKey: src.SecurityConfig.RealityPublicKey, + RealityShortId: src.SecurityConfig.RealityShortId, + Transport: src.Transport, + Host: src.TransportConfig.Host, + Path: src.TransportConfig.Path, + ServiceName: src.TransportConfig.ServiceName, + Flow: src.Flow, + Ratio: float64(info.TrafficRatio), + } + protocols = append(protocols, protocol) + case Hysteria2: + var src server.Hysteria2 + err := json.Unmarshal([]byte(info.Config), &src) + if err != nil { + return nil, err + } + protocol := node.Protocol{ + Type: "hysteria", + Port: uint16(src.Port), + HopPorts: src.HopPorts, + HopInterval: src.HopInterval, + ObfsPassword: src.ObfsPassword, + SNI: src.SecurityConfig.SNI, + AllowInsecure: src.SecurityConfig.AllowInsecure, + Fingerprint: src.SecurityConfig.Fingerprint, + RealityServerAddr: src.SecurityConfig.RealityServerAddr, + RealityServerPort: src.SecurityConfig.RealityServerPort, + RealityPrivateKey: src.SecurityConfig.RealityPrivateKey, + RealityPublicKey: src.SecurityConfig.RealityPublicKey, + RealityShortId: src.SecurityConfig.RealityShortId, + Ratio: float64(info.TrafficRatio), + } + protocols = append(protocols, protocol) + case Tuic: + var src server.Tuic + err := json.Unmarshal([]byte(info.Config), &src) + if err != nil { + return nil, err + } + protocol := node.Protocol{ + Type: "tuic", + Port: uint16(src.Port), + DisableSNI: src.DisableSNI, + ReduceRtt: src.ReduceRtt, + UDPRelayMode: src.UDPRelayMode, + CongestionController: src.CongestionController, + SNI: src.SecurityConfig.SNI, + AllowInsecure: src.SecurityConfig.AllowInsecure, + Fingerprint: src.SecurityConfig.Fingerprint, + RealityServerAddr: src.SecurityConfig.RealityServerAddr, + RealityServerPort: src.SecurityConfig.RealityServerPort, + RealityPrivateKey: src.SecurityConfig.RealityPrivateKey, + RealityPublicKey: src.SecurityConfig.RealityPublicKey, + RealityShortId: src.SecurityConfig.RealityShortId, + Ratio: float64(info.TrafficRatio), + } + protocols = append(protocols, protocol) + case AnyTLS: + var src server.AnyTLS + err := json.Unmarshal([]byte(info.Config), &src) + if err != nil { + return nil, err + } + protocol := node.Protocol{ + Type: "anytls", + Port: uint16(src.Port), + SNI: src.SecurityConfig.SNI, + AllowInsecure: src.SecurityConfig.AllowInsecure, + Fingerprint: src.SecurityConfig.Fingerprint, + RealityServerAddr: src.SecurityConfig.RealityServerAddr, + RealityServerPort: src.SecurityConfig.RealityServerPort, + RealityPrivateKey: src.SecurityConfig.RealityPrivateKey, + RealityPublicKey: src.SecurityConfig.RealityPublicKey, + RealityShortId: src.SecurityConfig.RealityShortId, + Ratio: float64(info.TrafficRatio), + } + protocols = append(protocols, protocol) + } + if len(protocols) > 0 { + err := result.MarshalProtocols(protocols) + if err != nil { + return nil, err + } + } + + return result, nil +} + +func (l *MigrateServerNodeLogic) adapterNode(info *server.Server) ([]*node.Node, error) { + var nodes []*node.Node + enable := true + switch info.RelayMode { + case server.RelayModeNone: + nodes = append(nodes, &node.Node{ + Name: info.Name, + Tags: "", + Port: 0, + Address: info.ServerAddr, + ServerId: info.Id, + Protocol: info.Protocol, + Enabled: &enable, + }) + default: + var relays []server.NodeRelay + err := json.Unmarshal([]byte(info.RelayNode), &relays) + if err != nil { + return nil, err + } + for _, relay := range relays { + nodes = append(nodes, &node.Node{ + Name: relay.Prefix + info.Name, + Tags: "", + Port: uint16(relay.Port), + Address: relay.Host, + ServerId: info.Id, + Protocol: info.Protocol, + Enabled: &enable, + }) + } + } + + return nodes, nil +} diff --git a/internal/logic/admin/server/queryNodeTagLogic.go b/internal/logic/admin/server/queryNodeTagLogic.go new file mode 100644 index 0000000..47e0daf --- /dev/null +++ b/internal/logic/admin/server/queryNodeTagLogic.go @@ -0,0 +1,46 @@ +package server + +import ( + "context" + "strings" + + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type QueryNodeTagLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewQueryNodeTagLogic Query all node tags +func NewQueryNodeTagLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryNodeTagLogic { + return &QueryNodeTagLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryNodeTagLogic) QueryNodeTag() (resp *types.QueryNodeTagResponse, err error) { + + var nodes []*node.Node + if err = l.svcCtx.DB.WithContext(l.ctx).Model(&node.Node{}).Find(&nodes).Error; err != nil { + l.Errorw("[QueryNodeTag] Query Database Error: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "[QueryNodeTag] Query Database Error") + } + var tags []string + for _, item := range nodes { + tags = append(tags, strings.Split(item.Tags, ",")...) + } + + return &types.QueryNodeTagResponse{ + Tags: tool.RemoveDuplicateElements(tags...), + }, nil +} diff --git a/internal/logic/admin/server/nodeSortLogic.go b/internal/logic/admin/server/resetSortWithNodeLogic.go similarity index 68% rename from internal/logic/admin/server/nodeSortLogic.go rename to internal/logic/admin/server/resetSortWithNodeLogic.go index 1141ca9..3866f54 100644 --- a/internal/logic/admin/server/nodeSortLogic.go +++ b/internal/logic/admin/server/resetSortWithNodeLogic.go @@ -3,36 +3,35 @@ package server import ( "context" - "github.com/perfect-panel/server/internal/model/server" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" - "gorm.io/gorm" - + "github.com/perfect-panel/server/internal/model/node" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" ) -type NodeSortLogic struct { +type ResetSortWithNodeLogic struct { logger.Logger ctx context.Context svcCtx *svc.ServiceContext } -// Node sort -func NewNodeSortLogic(ctx context.Context, svcCtx *svc.ServiceContext) *NodeSortLogic { - return &NodeSortLogic{ +// NewResetSortWithNodeLogic Reset node sort +func NewResetSortWithNodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ResetSortWithNodeLogic { + return &ResetSortWithNodeLogic{ Logger: logger.WithContext(ctx), ctx: ctx, svcCtx: svcCtx, } } -func (l *NodeSortLogic) NodeSort(req *types.NodeSortRequest) error { - err := l.svcCtx.ServerModel.Transaction(l.ctx, func(db *gorm.DB) error { +func (l *ResetSortWithNodeLogic) ResetSortWithNode(req *types.ResetSortRequest) error { + err := l.svcCtx.NodeModel.Transaction(l.ctx, func(db *gorm.DB) error { // find all servers id var existingIDs []int64 - db.Model(&server.Server{}).Select("id").Find(&existingIDs) + db.Model(&node.Node{}).Select("id").Find(&existingIDs) // check if the id is valid validIDMap := make(map[int64]bool) for _, id := range existingIDs { @@ -46,12 +45,12 @@ func (l *NodeSortLogic) NodeSort(req *types.NodeSortRequest) error { } } // query all servers - var servers []*server.Server - db.Model(&server.Server{}).Order("sort ASC").Find(&servers) + var servers []*node.Node + db.Model(&node.Node{}).Order("sort ASC").Find(&servers) // create a map of the current sort currentSortMap := make(map[int64]int64) for _, item := range servers { - currentSortMap[item.Id] = item.Sort + currentSortMap[item.Id] = int64(item.Sort) } // new sort map @@ -67,12 +66,12 @@ func (l *NodeSortLogic) NodeSort(req *types.NodeSortRequest) error { } } for _, item := range itemsToUpdate { - s, err := l.svcCtx.ServerModel.FindOne(l.ctx, item.Id) + s, err := l.svcCtx.NodeModel.FindOneNode(l.ctx, item.Id) if err != nil { return err } - s.Sort = item.Sort - if err := l.svcCtx.ServerModel.Update(l.ctx, s, db); err != nil { + s.Sort = int(item.Sort) + if err = l.svcCtx.NodeModel.UpdateNode(l.ctx, s, db); err != nil { l.Errorw("[NodeSort] Update Database Error: ", logger.Field("error", err.Error()), logger.Field("id", item.Id), logger.Field("sort", item.Sort)) return err } diff --git a/internal/logic/admin/server/resetSortWithServerLogic.go b/internal/logic/admin/server/resetSortWithServerLogic.go new file mode 100644 index 0000000..3fbe237 --- /dev/null +++ b/internal/logic/admin/server/resetSortWithServerLogic.go @@ -0,0 +1,86 @@ +package server + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type ResetSortWithServerLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewResetSortWithServerLogic Reset server sort +func NewResetSortWithServerLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ResetSortWithServerLogic { + return &ResetSortWithServerLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *ResetSortWithServerLogic) ResetSortWithServer(req *types.ResetSortRequest) error { + err := l.svcCtx.NodeModel.Transaction(l.ctx, func(db *gorm.DB) error { + // find all servers id + var existingIDs []int64 + db.Model(&node.Server{}).Select("id").Find(&existingIDs) + // check if the id is valid + validIDMap := make(map[int64]bool) + for _, id := range existingIDs { + validIDMap[id] = true + } + // check if the sort is valid + var validItems []types.SortItem + for _, item := range req.Sort { + if validIDMap[item.Id] { + validItems = append(validItems, item) + } + } + // query all servers + var servers []*node.Server + db.Model(&node.Server{}).Order("sort ASC").Find(&servers) + // create a map of the current sort + currentSortMap := make(map[int64]int64) + for _, item := range servers { + currentSortMap[item.Id] = int64(item.Sort) + } + + // new sort map + newSortMap := make(map[int64]int64) + for _, item := range validItems { + newSortMap[item.Id] = item.Sort + } + + var itemsToUpdate []types.SortItem + for _, item := range validItems { + if oldSort, exists := currentSortMap[item.Id]; exists && oldSort != item.Sort { + itemsToUpdate = append(itemsToUpdate, item) + } + } + for _, item := range itemsToUpdate { + s, err := l.svcCtx.NodeModel.FindOneServer(l.ctx, item.Id) + if err != nil { + return err + } + s.Sort = int(item.Sort) + if err = l.svcCtx.NodeModel.UpdateServer(l.ctx, s, db); err != nil { + l.Errorw("[NodeSort] Update Database Error: ", logger.Field("error", err.Error()), logger.Field("id", item.Id), logger.Field("sort", item.Sort)) + return err + } + } + return nil + }) + if err != nil { + l.Errorw("[NodeSort] Update Database Error: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), err.Error()) + } + return nil +} diff --git a/internal/logic/admin/server/toggleNodeStatusLogic.go b/internal/logic/admin/server/toggleNodeStatusLogic.go new file mode 100644 index 0000000..f71048f --- /dev/null +++ b/internal/logic/admin/server/toggleNodeStatusLogic.go @@ -0,0 +1,51 @@ +package server + +import ( + "context" + "strings" + + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type ToggleNodeStatusLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewToggleNodeStatusLogic Toggle Node Status +func NewToggleNodeStatusLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ToggleNodeStatusLogic { + return &ToggleNodeStatusLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *ToggleNodeStatusLogic) ToggleNodeStatus(req *types.ToggleNodeStatusRequest) error { + data, err := l.svcCtx.NodeModel.FindOneNode(l.ctx, req.Id) + if err != nil { + l.Errorw("[ToggleNodeStatus] Query Database Error: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "[ToggleNodeStatus] Query Database Error") + } + data.Enabled = req.Enable + + err = l.svcCtx.NodeModel.UpdateNode(l.ctx, data) + if err != nil { + l.Errorw("[ToggleNodeStatus] Update Database Error: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "[ToggleNodeStatus] Update Database Error") + } + + return l.svcCtx.NodeModel.ClearNodeCache(l.ctx, &node.FilterNodeParams{ + Page: 1, + Size: 1000, + ServerId: []int64{data.ServerId}, + Tag: strings.Split(data.Tags, ","), + Search: "", + }) +} diff --git a/internal/logic/admin/server/updateNodeGroupLogic.go b/internal/logic/admin/server/updateNodeGroupLogic.go deleted file mode 100644 index 5df203b..0000000 --- a/internal/logic/admin/server/updateNodeGroupLogic.go +++ /dev/null @@ -1,40 +0,0 @@ -package server - -import ( - "context" - - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" -) - -type UpdateNodeGroupLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewUpdateNodeGroupLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateNodeGroupLogic { - return &UpdateNodeGroupLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *UpdateNodeGroupLogic) UpdateNodeGroup(req *types.UpdateNodeGroupRequest) error { - // check server group exist - nodeGroup, err := l.svcCtx.ServerModel.FindOneGroup(l.ctx, req.Id) - if err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), err.Error()) - } - nodeGroup.Name = req.Name - nodeGroup.Description = req.Description - err = l.svcCtx.ServerModel.UpdateGroup(l.ctx, nodeGroup) - if err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), err.Error()) - } - return nil -} diff --git a/internal/logic/admin/server/updateNodeLogic.go b/internal/logic/admin/server/updateNodeLogic.go index f34ebe4..2af8a4d 100644 --- a/internal/logic/admin/server/updateNodeLogic.go +++ b/internal/logic/admin/server/updateNodeLogic.go @@ -2,13 +2,8 @@ package server import ( "context" - "encoding/json" - "strings" - - "github.com/hibiken/asynq" - "github.com/perfect-panel/server/pkg/device" - queue "github.com/perfect-panel/server/queue/types" + "github.com/perfect-panel/server/internal/model/node" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/logger" @@ -23,6 +18,7 @@ type UpdateNodeLogic struct { svcCtx *svc.ServiceContext } +// NewUpdateNodeLogic Update Node func NewUpdateNodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateNodeLogic { return &UpdateNodeLogic{ Logger: logger.WithContext(ctx), @@ -32,107 +28,27 @@ func NewUpdateNodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Update } func (l *UpdateNodeLogic) UpdateNode(req *types.UpdateNodeRequest) error { - // Check server exist - nodeInfo, err := l.svcCtx.ServerModel.FindOne(l.ctx, req.Id) + data, err := l.svcCtx.NodeModel.FindOneNode(l.ctx, req.Id) if err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find server error: %v", err) + l.Errorw("[UpdateNode] Query Database Error: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "[UpdateNode] Query Database Error") } - tool.DeepCopy(nodeInfo, req, tool.CopyWithIgnoreEmpty(false)) - config, err := json.Marshal(req.Config) - if err != nil { - return err - } - - nodeInfo.Config = string(config) - nodeRelay, err := json.Marshal(req.RelayNode) - if err != nil { - l.Errorw("[UpdateNode] Marshal RelayNode Error: ", logger.Field("error", err.Error())) - return err - } - - // 处理Tags字段 - switch { - case len(req.Tags) > 0: - // 有Tags,进行连接 - nodeInfo.Tags = strings.Join(req.Tags, ",") - default: - // 空数组,清空Tags - nodeInfo.Tags = "" - } - - nodeInfo.City = req.City - nodeInfo.Country = req.Country - - nodeInfo.RelayNode = string(nodeRelay) - if req.Protocol == "vless" { - var cfg types.Vless - if err := json.Unmarshal(config, &cfg); err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "json.Unmarshal error: %v", err.Error()) - } - if cfg.Security == "reality" && cfg.SecurityConfig.RealityPublicKey == "" { - public, private, err := tool.Curve25519Genkey(false, "") - if err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "generate curve25519 key error") - } - cfg.SecurityConfig.RealityPublicKey = public - cfg.SecurityConfig.RealityPrivateKey = private - cfg.SecurityConfig.RealityShortId = tool.GenerateShortID(private) - } - if cfg.SecurityConfig.RealityServerAddr == "" { - cfg.SecurityConfig.RealityServerAddr = cfg.SecurityConfig.SNI - } - if cfg.SecurityConfig.RealityServerPort == 0 { - cfg.SecurityConfig.RealityServerPort = 443 - } - config, _ = json.Marshal(cfg) - nodeInfo.Config = string(config) - } else if req.Protocol == "shadowsocks" { - var cfg types.Shadowsocks - if err = json.Unmarshal(config, &cfg); err != nil { - l.Errorf("[CreateNode] Unmarshal Shadowsocks Config Error: %v", err.Error()) - return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "json.Unmarshal error: %v", err.Error()) - } - if strings.Contains(cfg.Method, "2022") { - var length int - switch cfg.Method { - case "2022-blake3-aes-128-gcm": - length = 16 - default: - length = 32 - } - if len(cfg.ServerKey) != length { - cfg.ServerKey = tool.GenerateCipher(cfg.ServerKey, length) - } - } - config, _ = json.Marshal(cfg) - nodeInfo.Config = string(config) - } - err = l.svcCtx.ServerModel.Update(l.ctx, nodeInfo) + data.Name = req.Name + data.Tags = tool.StringSliceToString(req.Tags) + data.ServerId = req.ServerId + data.Port = req.Port + data.Address = req.Address + data.Protocol = req.Protocol + data.Enabled = req.Enabled + err = l.svcCtx.NodeModel.UpdateNode(l.ctx, data) if err != nil { l.Errorw("[UpdateNode] Update Database Error: ", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create server error: %v", err) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "[UpdateNode] Update Database Error") } - - if req.City == "" || req.Country == "" { - // Marshal the task payload - payload, err := json.Marshal(queue.GetNodeCountry{ - Protocol: nodeInfo.Protocol, - ServerAddr: nodeInfo.ServerAddr, - }) - if err != nil { - l.Errorw("[GetNodeCountry]: Marshal Error", logger.Field("error", err.Error())) - return errors.Wrap(xerr.NewErrCode(xerr.ERROR), "Failed to marshal task payload") - } - // Create a queue task - task := asynq.NewTask(queue.ForthwithGetCountry, payload) - // Enqueue the task - taskInfo, err := l.svcCtx.Queue.Enqueue(task) - if err != nil { - l.Errorw("[GetNodeCountry]: Enqueue Error", logger.Field("error", err.Error()), logger.Field("payload", string(payload))) - return errors.Wrap(xerr.NewErrCode(xerr.ERROR), "Failed to enqueue task") - } - l.Infow("[GetNodeCountry]: Enqueue Success", logger.Field("taskID", taskInfo.ID), logger.Field("payload", string(payload))) - } - l.svcCtx.DeviceManager.Broadcast(device.SubscribeUpdate) - return nil + return l.svcCtx.NodeModel.ClearNodeCache(l.ctx, &node.FilterNodeParams{ + Page: 1, + Size: 1000, + ServerId: []int64{data.ServerId}, + Search: "", + }) } diff --git a/internal/logic/admin/server/updateRuleGroupLogic.go b/internal/logic/admin/server/updateRuleGroupLogic.go deleted file mode 100644 index 500ba02..0000000 --- a/internal/logic/admin/server/updateRuleGroupLogic.go +++ /dev/null @@ -1,58 +0,0 @@ -package server - -import ( - "context" - "strings" - - "github.com/perfect-panel/server/pkg/tool" - - "github.com/perfect-panel/server/internal/model/server" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" - - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" -) - -type UpdateRuleGroupLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// NewUpdateRuleGroupLogic Update rule group -func NewUpdateRuleGroupLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateRuleGroupLogic { - return &UpdateRuleGroupLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *UpdateRuleGroupLogic) UpdateRuleGroup(req *types.UpdateRuleGroupRequest) error { - rs, err := parseAndValidateRules(req.Rules, req.Name) - if err != nil { - return err - } - err = l.svcCtx.ServerModel.UpdateRuleGroup(l.ctx, &server.RuleGroup{ - Id: req.Id, - Icon: req.Icon, - Type: req.Type, - Name: req.Name, - Tags: tool.StringSliceToString(req.Tags), - Rules: strings.Join(rs, "\n"), - Default: req.Default, - Enable: req.Enable, - }) - if err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), err.Error()) - } - if req.Default { - if err = l.svcCtx.ServerModel.SetDefaultRuleGroup(l.ctx, req.Id); err != nil { - l.Errorf("SetDefaultRuleGroup error: %v", err.Error()) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), err.Error()) - } - } - return nil -} diff --git a/internal/logic/admin/server/updateServerLogic.go b/internal/logic/admin/server/updateServerLogic.go new file mode 100644 index 0000000..f419130 --- /dev/null +++ b/internal/logic/admin/server/updateServerLogic.go @@ -0,0 +1,119 @@ +package server + +import ( + "context" + "strings" + + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/ip" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type UpdateServerLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewUpdateServerLogic Update Server +func NewUpdateServerLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateServerLogic { + return &UpdateServerLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateServerLogic) UpdateServer(req *types.UpdateServerRequest) error { + data, err := l.svcCtx.NodeModel.FindOneServer(l.ctx, req.Id) + if err != nil { + l.Errorf("[UpdateServer] FindOneServer Error: %v", err.Error()) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find server error: %v", err.Error()) + } + data.Name = req.Name + data.Country = req.Country + data.City = req.City + // only update address when it's different + if req.Address != data.Address { + // query server ip location + result, err := ip.GetRegionByIp(req.Address) + if err != nil { + l.Errorf("[UpdateServer] GetRegionByIp Error: %v", err.Error()) + } else { + data.City = result.City + data.Country = result.Country + } + // update address + data.Address = req.Address + } + protocols := make([]node.Protocol, 0) + for _, item := range req.Protocols { + if item.Type == "" { + return errors.Wrapf(xerr.NewErrCodeMsg(xerr.InvalidParams, "protocols type is empty"), "protocols type is empty") + } + var protocol node.Protocol + tool.DeepCopy(&protocol, item) + + // VLESS Reality Key Generation + if protocol.Type == "vless" { + if protocol.Security == "reality" { + if protocol.RealityPublicKey == "" { + public, private, err := tool.Curve25519Genkey(false, "") + if err != nil { + l.Errorf("[CreateServer] Generate Reality Key Error: %v", err.Error()) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "generate reality key error: %v", err) + } + protocol.RealityPublicKey = public + protocol.RealityPrivateKey = private + protocol.RealityShortId = tool.GenerateShortID(private) + } + if protocol.RealityServerAddr == "" { + protocol.RealityServerAddr = protocol.SNI + } + if protocol.RealityServerPort == 0 { + protocol.RealityServerPort = 443 + } + } + + } + // ShadowSocks 2022 Key Generation + if protocol.Type == "shadowsocks" { + if strings.Contains(protocol.Cipher, "2022") { + var length int + switch protocol.Cipher { + case "2022-blake3-aes-128-gcm": + length = 16 + default: + length = 32 + } + if len(protocol.ServerKey) != length { + protocol.ServerKey = tool.GenerateCipher(protocol.ServerKey, length) + } + } + } + protocols = append(protocols, protocol) + } + err = data.MarshalProtocols(protocols) + if err != nil { + l.Errorf("[UpdateServer] Marshal Protocols Error: %v", err.Error()) + return errors.Wrapf(xerr.NewErrCodeMsg(xerr.InvalidParams, "protocols marshal error"), "protocols marshal error: %v", err) + } + + err = l.svcCtx.NodeModel.UpdateServer(l.ctx, data) + if err != nil { + l.Errorf("[UpdateServer] UpdateServer Error: %v", err.Error()) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update server error: %v", err.Error()) + } + + return l.svcCtx.NodeModel.ClearNodeCache(l.ctx, &node.FilterNodeParams{ + Page: 1, + Size: 1000, + ServerId: []int64{req.Id}, + Search: "", + }) +} diff --git a/internal/logic/admin/subscribe/createSubscribeLogic.go b/internal/logic/admin/subscribe/createSubscribeLogic.go index 729bf5b..bf50d6a 100644 --- a/internal/logic/admin/subscribe/createSubscribeLogic.go +++ b/internal/logic/admin/subscribe/createSubscribeLogic.go @@ -37,6 +37,7 @@ func (l *CreateSubscribeLogic) CreateSubscribe(req *types.CreateSubscribeRequest sub := &subscribe.Subscribe{ Id: 0, Name: req.Name, + Language: req.Language, Description: req.Description, UnitPrice: req.UnitPrice, UnitTime: req.UnitTime, @@ -47,9 +48,8 @@ func (l *CreateSubscribeLogic) CreateSubscribe(req *types.CreateSubscribeRequest SpeedLimit: req.SpeedLimit, DeviceLimit: req.DeviceLimit, Quota: req.Quota, - GroupId: req.GroupId, - ServerGroup: tool.Int64SliceToString(req.ServerGroup), - Server: tool.Int64SliceToString(req.Server), + Nodes: tool.Int64SliceToString(req.Nodes), + NodeTags: tool.StringSliceToString(req.NodeTags), Show: req.Show, Sell: req.Sell, Sort: 0, diff --git a/internal/logic/admin/subscribe/getSubscribeDetailsLogic.go b/internal/logic/admin/subscribe/getSubscribeDetailsLogic.go index 856cbf7..6defdf1 100644 --- a/internal/logic/admin/subscribe/getSubscribeDetailsLogic.go +++ b/internal/logic/admin/subscribe/getSubscribeDetailsLogic.go @@ -3,6 +3,7 @@ package subscribe import ( "context" "encoding/json" + "strings" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" @@ -41,7 +42,7 @@ func (l *GetSubscribeDetailsLogic) GetSubscribeDetails(req *types.GetSubscribeDe l.Logger.Error("[GetSubscribeDetailsLogic] JSON unmarshal failed: ", logger.Field("error", err.Error()), logger.Field("discount", sub.Discount)) } } - resp.Server = tool.StringToInt64Slice(sub.Server) - resp.ServerGroup = tool.StringToInt64Slice(sub.ServerGroup) + resp.Nodes = tool.StringToInt64Slice(sub.Nodes) + resp.NodeTags = strings.Split(sub.NodeTags, ",") return resp, nil } diff --git a/internal/logic/admin/subscribe/getSubscribeListLogic.go b/internal/logic/admin/subscribe/getSubscribeListLogic.go index 7168afb..e8c7866 100644 --- a/internal/logic/admin/subscribe/getSubscribeListLogic.go +++ b/internal/logic/admin/subscribe/getSubscribeListLogic.go @@ -3,7 +3,9 @@ package subscribe import ( "context" "encoding/json" + "strings" + "github.com/perfect-panel/server/internal/model/subscribe" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/logger" @@ -28,7 +30,12 @@ func NewGetSubscribeListLogic(ctx context.Context, svcCtx *svc.ServiceContext) * } func (l *GetSubscribeListLogic) GetSubscribeList(req *types.GetSubscribeListRequest) (resp *types.GetSubscribeListResponse, err error) { - total, list, err := l.svcCtx.SubscribeModel.QuerySubscribeListByPage(l.ctx, int(req.Page), int(req.Size), req.GroupId, req.Search) + total, list, err := l.svcCtx.SubscribeModel.FilterList(l.ctx, &subscribe.FilterParams{ + Page: int(req.Page), + Size: int(req.Size), + Language: req.Language, + Search: req.Search, + }) if err != nil { l.Logger.Error("[GetSubscribeListLogic] get subscribe list failed: ", logger.Field("error", err.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get subscribe list failed: %v", err.Error()) @@ -47,8 +54,8 @@ func (l *GetSubscribeListLogic) GetSubscribeList(req *types.GetSubscribeListRequ l.Logger.Error("[GetSubscribeListLogic] JSON unmarshal failed: ", logger.Field("error", err.Error()), logger.Field("discount", item.Discount)) } } - sub.Server = tool.StringToInt64Slice(item.Server) - sub.ServerGroup = tool.StringToInt64Slice(item.ServerGroup) + sub.Nodes = tool.StringToInt64Slice(item.Nodes) + sub.NodeTags = strings.Split(item.NodeTags, ",") resultList = append(resultList, sub) } @@ -59,8 +66,8 @@ func (l *GetSubscribeListLogic) GetSubscribeList(req *types.GetSubscribeListRequ } for i, item := range resultList { - if subscribe, ok := subscribeMaps[item.Id]; ok { - resultList[i].Sold = subscribe + if sub, ok := subscribeMaps[item.Id]; ok { + resultList[i].Sold = sub } } diff --git a/internal/logic/admin/subscribe/resetAllSubscribeTokenLogic.go b/internal/logic/admin/subscribe/resetAllSubscribeTokenLogic.go new file mode 100644 index 0000000..e7307a2 --- /dev/null +++ b/internal/logic/admin/subscribe/resetAllSubscribeTokenLogic.go @@ -0,0 +1,61 @@ +package subscribe + +import ( + "context" + "strconv" + "time" + + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/pkg/uuidx" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" + + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" +) + +type ResetAllSubscribeTokenLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Reset all subscribe tokens +func NewResetAllSubscribeTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ResetAllSubscribeTokenLogic { + return &ResetAllSubscribeTokenLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *ResetAllSubscribeTokenLogic) ResetAllSubscribeToken() (resp *types.ResetAllSubscribeTokenResponse, err error) { + var list []*user.Subscribe + tx := l.svcCtx.DB.WithContext(l.ctx).Begin() + // select all active and Finished subscriptions + if err = tx.Model(&user.Subscribe{}).Where("`status` IN ?", []int64{1, 2}).Find(&list).Error; err != nil { + logger.Errorf("[ResetAllSubscribeToken] Failed to fetch subscribe list: %v", err.Error()) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Failed to fetch subscribe list: %v", err.Error()) + } + + for _, sub := range list { + sub.Token = uuidx.SubscribeToken(strconv.FormatInt(time.Now().UnixMilli(), 10) + strconv.FormatInt(sub.Id, 10)) + sub.UUID = uuidx.NewUUID().String() + if err = tx.Model(&user.Subscribe{}).Where("id = ?", sub.Id).Save(sub).Error; err != nil { + tx.Rollback() + logger.Errorf("[ResetAllSubscribeToken] Failed to update subscribe token for ID %d: %v", sub.Id, err.Error()) + return &types.ResetAllSubscribeTokenResponse{ + Success: false, + }, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "Failed to update subscribe token for ID %d: %v", sub.Id, err.Error()) + } + } + if err = tx.Commit().Error; err != nil { + logger.Errorf("[ResetAllSubscribeToken] Failed to commit transaction: %v", err.Error()) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "Failed to commit transaction: %v", err.Error()) + } + + return &types.ResetAllSubscribeTokenResponse{ + Success: true, + }, nil +} diff --git a/internal/logic/admin/subscribe/subscribeSortLogic.go b/internal/logic/admin/subscribe/subscribeSortLogic.go index bbdeec9..9a5cb5b 100644 --- a/internal/logic/admin/subscribe/subscribeSortLogic.go +++ b/internal/logic/admin/subscribe/subscribeSortLogic.go @@ -3,6 +3,7 @@ package subscribe import ( "context" + "github.com/perfect-panel/server/internal/model/subscribe" "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "gorm.io/gorm" @@ -40,7 +41,11 @@ func (l *SubscribeSortLogic) SubscribeSort(req *types.SubscribeSortRequest) erro l.Logger.Error("[SubscribeSortLogic] query subscribe list by ids error: ", logger.Field("error", err.Error())) return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query subscribe list by ids error: %v", err.Error()) } - subs, err := l.svcCtx.SubscribeModel.QuerySubscribeListByIds(l.ctx, ids) + _, subs, err := l.svcCtx.SubscribeModel.FilterList(l.ctx, &subscribe.FilterParams{ + Page: 1, + Size: 9999, + Ids: ids, + }) if err != nil { l.Logger.Error("[SubscribeSortLogic] query subscribe list by ids error: ", logger.Field("error", err.Error())) return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query subscribe list by ids error: %v", err.Error()) diff --git a/internal/logic/admin/subscribe/updateSubscribeLogic.go b/internal/logic/admin/subscribe/updateSubscribeLogic.go index e04e4ef..060af5a 100644 --- a/internal/logic/admin/subscribe/updateSubscribeLogic.go +++ b/internal/logic/admin/subscribe/updateSubscribeLogic.go @@ -45,6 +45,7 @@ func (l *UpdateSubscribeLogic) UpdateSubscribe(req *types.UpdateSubscribeRequest sub := &subscribe.Subscribe{ Id: req.Id, Name: req.Name, + Language: req.Language, Description: req.Description, UnitPrice: req.UnitPrice, UnitTime: req.UnitTime, @@ -55,9 +56,8 @@ func (l *UpdateSubscribeLogic) UpdateSubscribe(req *types.UpdateSubscribeRequest SpeedLimit: req.SpeedLimit, DeviceLimit: req.DeviceLimit, Quota: req.Quota, - GroupId: req.GroupId, - ServerGroup: tool.Int64SliceToString(req.ServerGroup), - Server: tool.Int64SliceToString(req.Server), + Nodes: tool.Int64SliceToString(req.Nodes), + NodeTags: tool.StringSliceToString(req.NodeTags), Show: req.Show, Sell: req.Sell, Sort: req.Sort, diff --git a/internal/logic/admin/system/createApplicationLogic.go b/internal/logic/admin/system/createApplicationLogic.go deleted file mode 100644 index 6191edd..0000000 --- a/internal/logic/admin/system/createApplicationLogic.go +++ /dev/null @@ -1,125 +0,0 @@ -package system - -import ( - "context" - - "github.com/perfect-panel/server/internal/model/application" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" -) - -type CreateApplicationLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewCreateApplicationLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateApplicationLogic { - return &CreateApplicationLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *CreateApplicationLogic) CreateApplication(req *types.CreateApplicationRequest) error { - var ios []application.ApplicationVersion - if len(req.Platform.IOS) > 0 { - for _, ios_ := range req.Platform.IOS { - ios = append(ios, application.ApplicationVersion{ - Url: ios_.Url, - Version: ios_.Version, - Platform: "ios", - IsDefault: ios_.IsDefault, - Description: ios_.Description, - }) - } - } - - var mac []application.ApplicationVersion - if len(req.Platform.MacOS) > 0 { - for _, mac_ := range req.Platform.MacOS { - mac = append(mac, application.ApplicationVersion{ - Url: mac_.Url, - Version: mac_.Version, - Platform: "macos", - IsDefault: mac_.IsDefault, - Description: mac_.Description, - }) - } - } - - var linux []application.ApplicationVersion - if len(req.Platform.Linux) > 0 { - for _, linux_ := range req.Platform.Linux { - linux = append(linux, application.ApplicationVersion{ - Url: linux_.Url, - Version: linux_.Version, - Platform: "linux", - IsDefault: linux_.IsDefault, - Description: linux_.Description, - }) - } - } - - var android []application.ApplicationVersion - if len(req.Platform.Android) > 0 { - for _, android_ := range req.Platform.Android { - android = append(android, application.ApplicationVersion{ - Url: android_.Url, - Version: android_.Version, - Platform: "android", - IsDefault: android_.IsDefault, - Description: android_.Description, - }) - } - } - - var windows []application.ApplicationVersion - if len(req.Platform.Windows) > 0 { - for _, windows_ := range req.Platform.Windows { - windows = append(windows, application.ApplicationVersion{ - Url: windows_.Url, - Version: windows_.Version, - Platform: "windows", - IsDefault: windows_.IsDefault, - Description: windows_.Description, - }) - } - } - - var harmony []application.ApplicationVersion - if len(req.Platform.Harmony) > 0 { - for _, harmony_ := range req.Platform.Harmony { - harmony = append(harmony, application.ApplicationVersion{ - Url: harmony_.Url, - Version: harmony_.Version, - Platform: "harmony", - IsDefault: harmony_.IsDefault, - Description: harmony_.Description, - }) - } - } - var applicationVersions []application.ApplicationVersion - applicationVersions = append(applicationVersions, ios...) - applicationVersions = append(applicationVersions, mac...) - applicationVersions = append(applicationVersions, linux...) - applicationVersions = append(applicationVersions, android...) - applicationVersions = append(applicationVersions, windows...) - applicationVersions = append(applicationVersions, harmony...) - app := application.Application{ - Name: req.Name, - Icon: req.Icon, - SubscribeType: req.SubscribeType, - ApplicationVersions: applicationVersions, - } - err := l.svcCtx.ApplicationModel.Insert(l.ctx, &app) - if err != nil { - l.Errorw("[CreateApplicationLogic] create application error: ", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create application error: %v", err) - } - return nil -} diff --git a/internal/logic/admin/system/createApplicationVersionLogic.go b/internal/logic/admin/system/createApplicationVersionLogic.go deleted file mode 100644 index 34a8970..0000000 --- a/internal/logic/admin/system/createApplicationVersionLogic.go +++ /dev/null @@ -1,44 +0,0 @@ -package system - -import ( - "context" - - "github.com/perfect-panel/server/internal/model/application" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" -) - -type CreateApplicationVersionLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Create application version -func NewCreateApplicationVersionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateApplicationVersionLogic { - return &CreateApplicationVersionLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *CreateApplicationVersionLogic) CreateApplicationVersion(req *types.CreateApplicationVersionRequest) error { - create := &application.ApplicationVersion{ - Url: req.Url, - Platform: req.Platform, - Version: req.Version, - Description: req.Description, - IsDefault: req.IsDefault, - ApplicationId: req.ApplicationId, - } - err := l.svcCtx.ApplicationModel.InsertVersion(l.ctx, create) - if err != nil { - l.Errorw("[CreateApplicationVersion] create application version error: ", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create application version error: %v", err) - } - return nil -} diff --git a/internal/logic/admin/system/deleteApplicationLogic.go b/internal/logic/admin/system/deleteApplicationLogic.go deleted file mode 100644 index 6bd3836..0000000 --- a/internal/logic/admin/system/deleteApplicationLogic.go +++ /dev/null @@ -1,35 +0,0 @@ -package system - -import ( - "context" - - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" -) - -type DeleteApplicationLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewDeleteApplicationLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteApplicationLogic { - return &DeleteApplicationLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *DeleteApplicationLogic) DeleteApplication(req *types.DeleteApplicationRequest) error { - // delete application - err := l.svcCtx.ApplicationModel.Delete(l.ctx, req.Id) - if err != nil { - l.Errorw("[DeleteApplicationLogic] delete application error: ", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete application error: %v", err.Error()) - } - return nil -} diff --git a/internal/logic/admin/system/deleteApplicationVersionLogic.go b/internal/logic/admin/system/deleteApplicationVersionLogic.go deleted file mode 100644 index 822d764..0000000 --- a/internal/logic/admin/system/deleteApplicationVersionLogic.go +++ /dev/null @@ -1,36 +0,0 @@ -package system - -import ( - "context" - - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" -) - -type DeleteApplicationVersionLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Delete application -func NewDeleteApplicationVersionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteApplicationVersionLogic { - return &DeleteApplicationVersionLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *DeleteApplicationVersionLogic) DeleteApplicationVersion(req *types.DeleteApplicationVersionRequest) error { - // delete application - err := l.svcCtx.ApplicationModel.DeleteVersion(l.ctx, req.Id) - if err != nil { - l.Errorw("[DeleteApplicationVersion] delete application version error: ", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete application version error: %v", err.Error()) - } - return nil -} diff --git a/internal/logic/admin/system/getApplicationConfigLogic.go b/internal/logic/admin/system/getApplicationConfigLogic.go deleted file mode 100644 index 3d3759f..0000000 --- a/internal/logic/admin/system/getApplicationConfigLogic.go +++ /dev/null @@ -1,49 +0,0 @@ -package system - -import ( - "context" - "strings" - - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" - "gorm.io/gorm" - - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" -) - -type GetApplicationConfigLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// get application config -func NewGetApplicationConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetApplicationConfigLogic { - return &GetApplicationConfigLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *GetApplicationConfigLogic) GetApplicationConfig() (resp *types.ApplicationConfig, err error) { - resp = &types.ApplicationConfig{} - appConfig, err := l.svcCtx.ApplicationModel.FindOneConfig(l.ctx, 1) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - err = nil - return - } - l.Errorw("[GetApplicationConfig] Database Error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get app config error: %v", err.Error()) - } - resp.AppId = appConfig.AppId - resp.EncryptionKey = appConfig.EncryptionKey - resp.EncryptionMethod = appConfig.EncryptionMethod - resp.Domains = strings.Split(appConfig.Domains, ";") - resp.StartupPicture = appConfig.StartupPicture - resp.StartupPictureSkipTime = appConfig.StartupPictureSkipTime - return -} diff --git a/internal/logic/admin/system/getApplicationLogic.go b/internal/logic/admin/system/getApplicationLogic.go deleted file mode 100644 index dfb6ab0..0000000 --- a/internal/logic/admin/system/getApplicationLogic.go +++ /dev/null @@ -1,113 +0,0 @@ -package system - -import ( - "context" - - "github.com/perfect-panel/server/internal/model/application" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" - "gorm.io/gorm" - - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" -) - -type GetApplicationLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Get application -func NewGetApplicationLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetApplicationLogic { - return &GetApplicationLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *GetApplicationLogic) GetApplication() (resp *types.ApplicationResponse, err error) { - resp = &types.ApplicationResponse{} - var applications []*application.Application - err = l.svcCtx.ApplicationModel.Transaction(l.ctx, func(tx *gorm.DB) (err error) { - return tx.Model(applications).Preload("ApplicationVersions").Find(&applications).Error - }) - if err != nil { - l.Errorw("[GetApplicationLogic] get application error: ", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get application error: %v", err.Error()) - } - - if len(applications) == 0 { - return resp, nil - } - - for _, app := range applications { - applicationResponse := types.ApplicationResponseInfo{ - Id: app.Id, - Name: app.Name, - Icon: app.Icon, - Description: app.Description, - SubscribeType: app.SubscribeType, - } - applicationVersions := app.ApplicationVersions - if len(applicationVersions) != 0 { - for _, applicationVersion := range applicationVersions { - switch applicationVersion.Platform { - case "ios": - applicationResponse.Platform.IOS = append(applicationResponse.Platform.IOS, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - case "macos": - applicationResponse.Platform.MacOS = append(applicationResponse.Platform.MacOS, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - case "linux": - applicationResponse.Platform.Linux = append(applicationResponse.Platform.Linux, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - case "android": - applicationResponse.Platform.Android = append(applicationResponse.Platform.Android, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - case "windows": - applicationResponse.Platform.Windows = append(applicationResponse.Platform.Windows, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - case "harmony": - applicationResponse.Platform.Harmony = append(applicationResponse.Platform.Harmony, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - } - } - } - resp.Applications = append(resp.Applications, applicationResponse) - } - - return -} diff --git a/internal/logic/admin/system/getModuleConfigLogic.go b/internal/logic/admin/system/getModuleConfigLogic.go new file mode 100644 index 0000000..2dc646f --- /dev/null +++ b/internal/logic/admin/system/getModuleConfigLogic.go @@ -0,0 +1,41 @@ +package system + +import ( + "context" + "os" + + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetModuleConfigLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get Module Config +func NewGetModuleConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetModuleConfigLogic { + return &GetModuleConfigLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetModuleConfigLogic) GetModuleConfig() (resp *types.ModuleConfig, err error) { + value, exists := os.LookupEnv("SECRET_KEY") + if !exists { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), " SECRET_KEY not set in environment variables") + } + + return &types.ModuleConfig{ + Secret: value, + ServiceName: constant.ServiceName, + ServiceVersion: constant.Version, + }, nil +} diff --git a/internal/logic/admin/system/getNodeConfigLogic.go b/internal/logic/admin/system/getNodeConfigLogic.go index b33e180..1212b1a 100644 --- a/internal/logic/admin/system/getNodeConfigLogic.go +++ b/internal/logic/admin/system/getNodeConfigLogic.go @@ -2,7 +2,8 @@ package system import ( "context" - + "encoding/json" + "github.com/perfect-panel/server/internal/config" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/logger" @@ -26,15 +27,45 @@ func NewGetNodeConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Get } func (l *GetNodeConfigLogic) GetNodeConfig() (*types.NodeConfig, error) { - resp := &types.NodeConfig{} - // get server config from db configs, err := l.svcCtx.SystemModel.GetNodeConfig(l.ctx) if err != nil { l.Errorw("[GetNodeConfigLogic] GetNodeConfig get server config error: ", logger.Field("error", err.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetNodeConfig get server config error: %v", err.Error()) } - // reflect to response - tool.SystemConfigSliceReflectToStruct(configs, resp) - return resp, nil + var dbConfig config.NodeDBConfig + tool.SystemConfigSliceReflectToStruct(configs, &dbConfig) + c := &types.NodeConfig{ + NodeSecret: dbConfig.NodeSecret, + NodePullInterval: dbConfig.NodePullInterval, + NodePushInterval: dbConfig.NodePushInterval, + IPStrategy: dbConfig.IPStrategy, + TrafficReportThreshold: dbConfig.TrafficReportThreshold, + } + + if dbConfig.DNS != "" { + var dns []types.NodeDNS + err = json.Unmarshal([]byte(dbConfig.DNS), &dns) + if err != nil { + logger.Errorf("[Node] Unmarshal DNS config error: %s", err.Error()) + panic(err) + } + c.DNS = dns + } + if dbConfig.Block != "" { + var block []string + _ = json.Unmarshal([]byte(dbConfig.Block), &block) + c.Block = tool.RemoveDuplicateElements(block...) + } + if dbConfig.Outbound != "" { + var outbound []types.NodeOutbound + err = json.Unmarshal([]byte(dbConfig.Outbound), &outbound) + if err != nil { + logger.Errorf("[Node] Unmarshal Outbound config error: %s", err.Error()) + panic(err) + } + c.Outbound = outbound + } + + return c, nil } diff --git a/internal/logic/admin/system/getSubscribeTypeLogic.go b/internal/logic/admin/system/getSubscribeTypeLogic.go deleted file mode 100644 index b9309e6..0000000 --- a/internal/logic/admin/system/getSubscribeTypeLogic.go +++ /dev/null @@ -1,42 +0,0 @@ -package system - -import ( - "context" - - "github.com/perfect-panel/server/internal/model/subscribeType" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" -) - -type GetSubscribeTypeLogic struct { - ctx context.Context - svcCtx *svc.ServiceContext - logger.Logger -} - -func NewGetSubscribeTypeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetSubscribeTypeLogic { - return &GetSubscribeTypeLogic{ - ctx: ctx, - svcCtx: svcCtx, - Logger: logger.WithContext(ctx), - } -} - -func (l *GetSubscribeTypeLogic) GetSubscribeType() (resp *types.SubscribeType, err error) { - var list []*subscribeType.SubscribeType - err = l.svcCtx.DB.Model(&subscribeType.SubscribeType{}).Find(&list).Error - if err != nil { - l.Errorw("[GetSubscribeType] get subscribe type failed", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get subscribe type failed: %v", err) - } - typeList := make([]string, 0) - for _, item := range list { - typeList = append(typeList, item.Name) - } - return &types.SubscribeType{ - SubscribeTypes: typeList, - }, nil -} diff --git a/internal/logic/admin/system/preViewNodeMultiplierLogic.go b/internal/logic/admin/system/preViewNodeMultiplierLogic.go new file mode 100644 index 0000000..58d1dd5 --- /dev/null +++ b/internal/logic/admin/system/preViewNodeMultiplierLogic.go @@ -0,0 +1,33 @@ +package system + +import ( + "context" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "time" +) + +type PreViewNodeMultiplierLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// PreView Node Multiplier +func NewPreViewNodeMultiplierLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PreViewNodeMultiplierLogic { + return &PreViewNodeMultiplierLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *PreViewNodeMultiplierLogic) PreViewNodeMultiplier() (resp *types.PreViewNodeMultiplierResponse, err error) { + now := time.Now() + ratio := l.svcCtx.NodeMultiplierManager.GetMultiplier(now) + return &types.PreViewNodeMultiplierResponse{ + Ratio: ratio, + CurrentTime: now.Format("2006-01-02 15:04:05"), + }, nil +} diff --git a/internal/logic/admin/system/updateApplicationConfigLogic.go b/internal/logic/admin/system/updateApplicationConfigLogic.go deleted file mode 100644 index 915aa38..0000000 --- a/internal/logic/admin/system/updateApplicationConfigLogic.go +++ /dev/null @@ -1,45 +0,0 @@ -package system - -import ( - "context" - "strings" - - "github.com/perfect-panel/server/internal/model/application" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" -) - -type UpdateApplicationConfigLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// update application config -func NewUpdateApplicationConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateApplicationConfigLogic { - return &UpdateApplicationConfigLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *UpdateApplicationConfigLogic) UpdateApplicationConfig(req *types.ApplicationConfig) error { - err := l.svcCtx.ApplicationModel.UpdateConfig(l.ctx, &application.ApplicationConfig{ - Id: 1, - AppId: req.AppId, - EncryptionKey: req.EncryptionKey, - EncryptionMethod: req.EncryptionMethod, - Domains: strings.Join(req.Domains, ";"), - StartupPicture: req.StartupPicture, - StartupPictureSkipTime: req.StartupPictureSkipTime, - }) - if err != nil { - l.Errorw("[UpdateApplicationConfig] Database Error", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update app config error: %v", err.Error()) - } - return nil -} diff --git a/internal/logic/admin/system/updateApplicationLogic.go b/internal/logic/admin/system/updateApplicationLogic.go deleted file mode 100644 index f867ab8..0000000 --- a/internal/logic/admin/system/updateApplicationLogic.go +++ /dev/null @@ -1,149 +0,0 @@ -package system - -import ( - "context" - - "github.com/perfect-panel/server/internal/model/application" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" - "gorm.io/gorm" - - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" -) - -type UpdateApplicationLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewUpdateApplicationLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateApplicationLogic { - return &UpdateApplicationLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *UpdateApplicationLogic) UpdateApplication(req *types.UpdateApplicationRequest) error { - - // find application - app, err := l.svcCtx.ApplicationModel.FindOne(l.ctx, req.Id) - if err != nil { - l.Errorw("[UpdateApplication] find application error", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find application error: %v", err.Error()) - } - app.Name = req.Name - app.Icon = req.Icon - app.SubscribeType = req.SubscribeType - app.Description = req.Description - - var ios []application.ApplicationVersion - if len(req.Platform.IOS) > 0 { - for _, ios_ := range req.Platform.IOS { - ios = append(ios, application.ApplicationVersion{ - Url: ios_.Url, - Version: ios_.Version, - Platform: "ios", - IsDefault: ios_.IsDefault, - Description: ios_.Description, - ApplicationId: app.Id, - }) - } - } - - var mac []application.ApplicationVersion - if len(req.Platform.MacOS) > 0 { - for _, mac_ := range req.Platform.MacOS { - mac = append(mac, application.ApplicationVersion{ - Url: mac_.Url, - Version: mac_.Version, - Platform: "macos", - IsDefault: mac_.IsDefault, - Description: mac_.Description, - ApplicationId: app.Id, - }) - } - } - - var linux []application.ApplicationVersion - if len(req.Platform.Linux) > 0 { - for _, linux_ := range req.Platform.Linux { - linux = append(linux, application.ApplicationVersion{ - Url: linux_.Url, - Version: linux_.Version, - Platform: "linux", - IsDefault: linux_.IsDefault, - Description: linux_.Description, - ApplicationId: app.Id, - }) - } - } - - var android []application.ApplicationVersion - if len(req.Platform.Android) > 0 { - for _, android_ := range req.Platform.Android { - android = append(android, application.ApplicationVersion{ - Url: android_.Url, - Version: android_.Version, - Platform: "android", - IsDefault: android_.IsDefault, - Description: android_.Description, - ApplicationId: app.Id, - }) - } - } - - var windows []application.ApplicationVersion - if len(req.Platform.Windows) > 0 { - for _, windows_ := range req.Platform.Windows { - windows = append(windows, application.ApplicationVersion{ - Url: windows_.Url, - Version: windows_.Version, - Platform: "windows", - IsDefault: windows_.IsDefault, - Description: windows_.Description, - ApplicationId: app.Id, - }) - } - } - - var harmony []application.ApplicationVersion - if len(req.Platform.Harmony) > 0 { - for _, harmony_ := range req.Platform.Harmony { - harmony = append(harmony, application.ApplicationVersion{ - Url: harmony_.Url, - Version: harmony_.Version, - Platform: "harmony", - IsDefault: harmony_.IsDefault, - Description: harmony_.Description, - ApplicationId: app.Id, - }) - } - } - var applicationVersions []application.ApplicationVersion - applicationVersions = append(applicationVersions, ios...) - applicationVersions = append(applicationVersions, mac...) - applicationVersions = append(applicationVersions, linux...) - applicationVersions = append(applicationVersions, android...) - applicationVersions = append(applicationVersions, windows...) - applicationVersions = append(applicationVersions, harmony...) - app.ApplicationVersions = applicationVersions - err = l.svcCtx.ApplicationModel.Transaction(l.ctx, func(db *gorm.DB) error { - - if err = db.Where("application_id = ?", app.Id).Delete(&application.ApplicationVersion{}).Error; err != nil { - return err - } - if err = db.Create(&applicationVersions).Error; err != nil { - return err - } - return db.Save(app).Error - }) - if err != nil { - l.Errorw("[UpdateApplication] update application error", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update application error: %v", err.Error()) - } - return nil -} diff --git a/internal/logic/admin/system/updateApplicationVersionLogic.go b/internal/logic/admin/system/updateApplicationVersionLogic.go deleted file mode 100644 index 4730ba4..0000000 --- a/internal/logic/admin/system/updateApplicationVersionLogic.go +++ /dev/null @@ -1,45 +0,0 @@ -package system - -import ( - "context" - - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" -) - -type UpdateApplicationVersionLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Update application version -func NewUpdateApplicationVersionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateApplicationVersionLogic { - return &UpdateApplicationVersionLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *UpdateApplicationVersionLogic) UpdateApplicationVersion(req *types.UpdateApplicationVersionRequest) error { - // find application - app, err := l.svcCtx.ApplicationModel.FindOneVersion(l.ctx, req.Id) - if err != nil { - l.Errorw("[UpdateApplicationVersion] find application version error", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find application error: %v", err.Error()) - } - app.Url = req.Url - app.Version = req.Version - app.Description = req.Description - app.IsDefault = req.IsDefault - err = l.svcCtx.ApplicationModel.UpdateVersion(l.ctx, app) - if err != nil { - l.Errorw("[UpdateApplicationVersion] update application version error", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update application version error: %v", err.Error()) - } - return nil -} diff --git a/internal/logic/admin/system/updateNodeConfigLogic.go b/internal/logic/admin/system/updateNodeConfigLogic.go index 128342f..dbbba84 100644 --- a/internal/logic/admin/system/updateNodeConfigLogic.go +++ b/internal/logic/admin/system/updateNodeConfigLogic.go @@ -41,7 +41,9 @@ func (l *UpdateNodeConfigLogic) UpdateNodeConfig(req *types.NodeConfig) error { // Get the field name fieldName := t.Field(i).Name // Get the field value to string - fieldValue := tool.ConvertValueToString(v.Field(i)) + var fieldValue string + + fieldValue = tool.ConvertValueToString(v.Field(i)) // Update the server config err = db.Model(&system.System{}).Where("`category` = 'server' and `key` = ?", fieldName).Update("value", fieldValue).Error if err != nil { diff --git a/internal/logic/admin/tool/getVersionLogic.go b/internal/logic/admin/tool/getVersionLogic.go new file mode 100644 index 0000000..a1a9acc --- /dev/null +++ b/internal/logic/admin/tool/getVersionLogic.go @@ -0,0 +1,51 @@ +package tool + +import ( + "context" + "fmt" + + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/logger" +) + +type GetVersionLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewGetVersionLogic Get Version +func NewGetVersionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetVersionLogic { + return &GetVersionLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetVersionLogic) GetVersion() (resp *types.VersionResponse, err error) { + version := constant.Version + buildTime := constant.BuildTime + + // Normalize unknown values + if version == "unknown version" { + version = "unknown" + } + if buildTime == "unknown time" { + buildTime = "unknown" + } + + // Format version based on whether it starts with 'v' + var formattedVersion string + if len(version) > 0 && version[0] == 'v' { + formattedVersion = fmt.Sprintf("%s(%s)", version[1:], buildTime) + } else { + formattedVersion = fmt.Sprintf("%s(%s) Develop", version, buildTime) + } + + return &types.VersionResponse{ + Version: formattedVersion, + }, nil +} diff --git a/internal/logic/admin/tool/queryIPLocationLogic.go b/internal/logic/admin/tool/queryIPLocationLogic.go new file mode 100644 index 0000000..6487b05 --- /dev/null +++ b/internal/logic/admin/tool/queryIPLocationLogic.go @@ -0,0 +1,57 @@ +package tool + +import ( + "context" + "net" + + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type QueryIPLocationLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewQueryIPLocationLogic Query IP Location +func NewQueryIPLocationLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryIPLocationLogic { + return &QueryIPLocationLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryIPLocationLogic) QueryIPLocation(req *types.QueryIPLocationRequest) (resp *types.QueryIPLocationResponse, err error) { + if l.svcCtx.GeoIP == nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), " GeoIP database not configured") + } + + ip := net.ParseIP(req.IP) + record, err := l.svcCtx.GeoIP.DB.City(ip) + if err != nil { + l.Errorf("Failed to query IP location: %v", err) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Failed to query IP location") + } + + var country, region, city string + if record.Country.Names != nil { + country = record.Country.Names["en"] + } + if len(record.Subdivisions) > 0 && record.Subdivisions[0].Names != nil { + region = record.Subdivisions[0].Names["en"] + } + if record.City.Names != nil { + city = record.City.Names["en"] + } + + return &types.QueryIPLocationResponse{ + Country: country, + Region: region, + City: city, + }, nil +} diff --git a/internal/logic/admin/user/createUserLogic.go b/internal/logic/admin/user/createUserLogic.go index 0bfdf9f..0fb6b43 100644 --- a/internal/logic/admin/user/createUserLogic.go +++ b/internal/logic/admin/user/createUserLogic.go @@ -39,10 +39,13 @@ func (l *CreateUserLogic) CreateUser(req *types.CreateUserRequest) error { } pwd := tool.EncodePassWord(req.Password) newUser := &user.User{ - Password: pwd, - ReferCode: req.ReferCode, - Balance: req.Balance, - IsAdmin: &req.IsAdmin, + Password: pwd, + Algo: "default", + ReferralPercentage: req.ReferralPercentage, + OnlyFirstPurchase: &req.OnlyFirstPurchase, + ReferCode: req.ReferCode, + Balance: req.Balance, + IsAdmin: &req.IsAdmin, } var ams []user.AuthMethods diff --git a/internal/logic/admin/user/createUserSubscribeLogic.go b/internal/logic/admin/user/createUserSubscribeLogic.go index 80e3f98..08876f8 100644 --- a/internal/logic/admin/user/createUserSubscribeLogic.go +++ b/internal/logic/admin/user/createUserSubscribeLogic.go @@ -77,5 +77,9 @@ func (l *CreateUserSubscribeLogic) CreateUserSubscribe(req *types.CreateUserSubs return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "UpdateUserCache error: %v", err.Error()) } + err = l.svcCtx.SubscribeModel.ClearCache(l.ctx, userSub.SubscribeId) + if err != nil { + logger.Errorw("ClearSubscribe error", logger.Field("error", err.Error())) + } return nil } diff --git a/internal/logic/admin/user/deleteUserSubscribeLogic.go b/internal/logic/admin/user/deleteUserSubscribeLogic.go index 6526829..397299d 100644 --- a/internal/logic/admin/user/deleteUserSubscribeLogic.go +++ b/internal/logic/admin/user/deleteUserSubscribeLogic.go @@ -26,10 +26,27 @@ func NewDeleteUserSubscribeLogic(ctx context.Context, svcCtx *svc.ServiceContext } func (l *DeleteUserSubscribeLogic) DeleteUserSubscribe(req *types.DeleteUserSubscribeRequest) error { - err := l.svcCtx.UserModel.DeleteSubscribeById(l.ctx, req.UserSubscribeId) + // find user subscribe by ID + userSubscribe, err := l.svcCtx.UserModel.FindOneSubscribe(l.ctx, req.UserSubscribeId) + if err != nil { + l.Errorw("failed to find user subscribe", logger.Field("error", err.Error()), logger.Field("userSubscribeId", req.UserSubscribeId)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "failed to find user subscribe: %v", err.Error()) + } + + err = l.svcCtx.UserModel.DeleteSubscribeById(l.ctx, req.UserSubscribeId) if err != nil { l.Errorw("failed to delete user subscribe", logger.Field("error", err.Error()), logger.Field("userSubscribeId", req.UserSubscribeId)) return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "failed to delete user subscribe: %v", err.Error()) } + // Clear user subscribe cache + if err = l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, userSubscribe); err != nil { + l.Errorw("failed to clear user subscribe cache", logger.Field("error", err.Error()), logger.Field("userSubscribeId", req.UserSubscribeId)) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "failed to clear user subscribe cache: %v", err.Error()) + } + // Clear subscribe cache + if err = l.svcCtx.SubscribeModel.ClearCache(l.ctx, userSubscribe.SubscribeId); err != nil { + l.Errorw("failed to clear subscribe cache", logger.Field("error", err.Error()), logger.Field("subscribeId", userSubscribe.SubscribeId)) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "failed to clear subscribe cache: %v", err.Error()) + } return nil } diff --git a/internal/logic/admin/user/getUserListLogic.go b/internal/logic/admin/user/getUserListLogic.go index a23f174..3859f76 100644 --- a/internal/logic/admin/user/getUserListLogic.go +++ b/internal/logic/admin/user/getUserListLogic.go @@ -32,6 +32,7 @@ func (l *GetUserListLogic) GetUserList(req *types.GetUserListRequest) (*types.Ge Search: req.Search, SubscribeId: req.SubscribeId, UserSubscribeId: req.UserSubscribeId, + Order: "DESC", }) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetUserListLogic failed: %v", err.Error()) @@ -40,20 +41,20 @@ func (l *GetUserListLogic) GetUserList(req *types.GetUserListRequest) (*types.Ge userRespList := make([]types.User, 0, len(list)) for _, item := range list { - var user types.User - tool.DeepCopy(&user, item) + var u types.User + tool.DeepCopy(&u, item) // 处理 AuthMethods - authMethods := make([]types.UserAuthMethod, len(user.AuthMethods)) // 直接创建目标 slice - for i, method := range user.AuthMethods { + authMethods := make([]types.UserAuthMethod, len(u.AuthMethods)) // 直接创建目标 slice + for i, method := range u.AuthMethods { tool.DeepCopy(&authMethods[i], method) if method.AuthType == "mobile" { authMethods[i].AuthIdentifier = phone.FormatToInternational(method.AuthIdentifier) } } - user.AuthMethods = authMethods + u.AuthMethods = authMethods - userRespList = append(userRespList, user) + userRespList = append(userRespList, u) } return &types.GetUserListResponse{ diff --git a/internal/logic/admin/user/getUserLoginLogsLogic.go b/internal/logic/admin/user/getUserLoginLogsLogic.go index f70fd24..afaba61 100644 --- a/internal/logic/admin/user/getUserLoginLogsLogic.go +++ b/internal/logic/admin/user/getUserLoginLogsLogic.go @@ -3,11 +3,10 @@ package user import ( "context" - "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/model/log" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/tool" "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -28,15 +27,34 @@ func NewGetUserLoginLogsLogic(ctx context.Context, svcCtx *svc.ServiceContext) * } func (l *GetUserLoginLogsLogic) GetUserLoginLogs(req *types.GetUserLoginLogsRequest) (resp *types.GetUserLoginLogsResponse, err error) { - data, total, err := l.svcCtx.UserModel.FilterLoginLogList(l.ctx, req.Page, req.Size, &user.LoginLogFilterParams{ - UserId: req.UserId, + data, total, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{ + Page: req.Page, + Size: req.Size, + Type: log.TypeLogin.Uint8(), + ObjectID: req.UserId, }) if err != nil { l.Errorw("[GetUserLoginLogs] get user login logs failed", logger.Field("error", err.Error()), logger.Field("request", req)) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get user login logs failed: %v", err.Error()) } var list []types.UserLoginLog - tool.DeepCopy(&list, data) + + for _, datum := range data { + var content log.Login + if err = content.Unmarshal([]byte(datum.Content)); err != nil { + l.Errorf("[GetUserLoginLogs] unmarshal login log content failed: %v", err.Error()) + continue + } + list = append(list, types.UserLoginLog{ + Id: datum.Id, + UserId: datum.ObjectID, + LoginIP: content.LoginIP, + UserAgent: content.UserAgent, + Success: content.Success, + Timestamp: datum.CreatedAt.UnixMilli(), + }) + } + return &types.GetUserLoginLogsResponse{ Total: total, List: list, diff --git a/internal/logic/admin/user/getUserSubscribeLogsLogic.go b/internal/logic/admin/user/getUserSubscribeLogsLogic.go index aa64873..ca33355 100644 --- a/internal/logic/admin/user/getUserSubscribeLogsLogic.go +++ b/internal/logic/admin/user/getUserSubscribeLogsLogic.go @@ -3,7 +3,7 @@ package user import ( "context" - "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/model/log" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/logger" @@ -28,10 +28,7 @@ func NewGetUserSubscribeLogsLogic(ctx context.Context, svcCtx *svc.ServiceContex } func (l *GetUserSubscribeLogsLogic) GetUserSubscribeLogs(req *types.GetUserSubscribeLogsRequest) (resp *types.GetUserSubscribeLogsResponse, err error) { - data, total, err := l.svcCtx.UserModel.FilterSubscribeLogList(l.ctx, req.Page, req.Size, &user.SubscribeLogFilterParams{ - UserSubscribeId: req.SubscribeId, - UserId: req.UserId, - }) + data, total, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{}) if err != nil { l.Errorw("[GetUserSubscribeLogs] Get User Subscribe Logs Error:", logger.Field("err", err.Error())) diff --git a/internal/logic/admin/user/getUserSubscribeResetTrafficLogsLogic.go b/internal/logic/admin/user/getUserSubscribeResetTrafficLogsLogic.go new file mode 100644 index 0000000..fb01d01 --- /dev/null +++ b/internal/logic/admin/user/getUserSubscribeResetTrafficLogsLogic.go @@ -0,0 +1,62 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetUserSubscribeResetTrafficLogsLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get user subcribe reset traffic logs +func NewGetUserSubscribeResetTrafficLogsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserSubscribeResetTrafficLogsLogic { + return &GetUserSubscribeResetTrafficLogsLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetUserSubscribeResetTrafficLogsLogic) GetUserSubscribeResetTrafficLogs(req *types.GetUserSubscribeResetTrafficLogsRequest) (resp *types.GetUserSubscribeResetTrafficLogsResponse, err error) { + data, total, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{ + Page: req.Page, + Size: req.Size, + Type: log.TypeResetSubscribe.Uint8(), + ObjectID: req.UserSubscribeId, + }) + if err != nil { + l.Errorf("[ResetSubscribeTrafficLog] failed to filter system log: %v", err) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FilterSystemLog failed, err: %v", err) + } + + var list []types.ResetSubscribeTrafficLog + + for _, item := range data { + var content log.ResetSubscribe + if err = content.Unmarshal([]byte(item.Content)); err != nil { + l.Errorf("[ResetSubscribeTrafficLog] failed to unmarshal log: %v", err) + continue + } + list = append(list, types.ResetSubscribeTrafficLog{ + Id: item.Id, + Type: content.Type, + OrderNo: content.OrderNo, + Timestamp: content.Timestamp, + UserSubscribeId: item.ObjectID, + }) + } + + return &types.GetUserSubscribeResetTrafficLogsResponse{ + Total: total, + List: list, + }, nil +} diff --git a/internal/logic/admin/user/updateUserAuthMethodLogic.go b/internal/logic/admin/user/updateUserAuthMethodLogic.go index 4cda05c..6b87523 100644 --- a/internal/logic/admin/user/updateUserAuthMethodLogic.go +++ b/internal/logic/admin/user/updateUserAuthMethodLogic.go @@ -31,11 +31,21 @@ func (l *UpdateUserAuthMethodLogic) UpdateUserAuthMethod(req *types.UpdateUserAu l.Errorw("Get user auth method error", logger.Field("error", err.Error()), logger.Field("userId", req.UserId), logger.Field("authType", req.AuthType)) return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Get user auth method error: %v", err.Error()) } + userInfo, err := l.svcCtx.UserModel.FindOne(l.ctx, req.UserId) + if err != nil { + l.Errorw("Get user info error", logger.Field("error", err.Error()), logger.Field("userId", req.UserId)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Get user info error: %v", err.Error()) + } + method.AuthType = req.AuthType method.AuthIdentifier = req.AuthIdentifier if err = l.svcCtx.UserModel.UpdateUserAuthMethods(l.ctx, method); err != nil { l.Errorw("Update user auth method error", logger.Field("error", err.Error()), logger.Field("userId", req.UserId), logger.Field("authType", req.AuthType)) return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "Update user auth method error: %v", err.Error()) } + if err = l.svcCtx.UserModel.UpdateUserCache(l.ctx, userInfo); err != nil { + l.Errorw("Update user cache error", logger.Field("error", err.Error()), logger.Field("userId", req.UserId)) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Update user cache error: %v", err.Error()) + } return nil } diff --git a/internal/logic/admin/user/updateUserBasicInfoLogic.go b/internal/logic/admin/user/updateUserBasicInfoLogic.go index fb673e4..faa7930 100644 --- a/internal/logic/admin/user/updateUserBasicInfoLogic.go +++ b/internal/logic/admin/user/updateUserBasicInfoLogic.go @@ -4,7 +4,9 @@ import ( "context" "os" "strings" + "time" + "github.com/perfect-panel/server/internal/model/log" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/logger" @@ -37,22 +39,97 @@ func (l *UpdateUserBasicInfoLogic) UpdateUserBasicInfo(req *types.UpdateUserBasi isDemo := strings.ToLower(os.Getenv("PPANEL_MODE")) == "demo" - tool.DeepCopy(userInfo, req) if req.Avatar != "" && !tool.IsValidImageSize(req.Avatar, 1024) { return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Invalid Image Size") } - userInfo.Balance = req.Balance - userInfo.GiftAmount = req.GiftAmount - userInfo.Commission = req.Commission - // 手动设置 IsAdmin 字段,因为类型不匹配(*bool vs bool) - userInfo.IsAdmin = &req.IsAdmin - userInfo.Enable = &req.Enable + + if userInfo.Balance != req.Balance { + change := req.Balance - userInfo.Balance + balanceLog := log.Balance{ + Type: log.BalanceTypeAdjust, + Amount: change, + OrderNo: "", + Balance: req.Balance, + Timestamp: time.Now().UnixMilli(), + } + content, _ := balanceLog.Marshal() + + err = l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + Type: log.TypeBalance.Uint8(), + Date: time.Now().Format(time.DateOnly), + ObjectID: userInfo.Id, + Content: string(content), + }) + if err != nil { + l.Errorw("[UpdateUserBasicInfoLogic] Insert Balance Log Error:", logger.Field("err", err.Error()), logger.Field("userId", req.UserId)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "Insert Balance Log Error") + } + userInfo.Balance = req.Balance + } + + if userInfo.GiftAmount != req.GiftAmount { + change := req.GiftAmount - userInfo.GiftAmount + if change != 0 { + var changeType uint16 + if userInfo.GiftAmount < req.GiftAmount { + changeType = log.GiftTypeIncrease + } else { + changeType = log.GiftTypeReduce + } + giftLog := log.Gift{ + Type: changeType, + Amount: change, + Balance: req.GiftAmount, + Remark: "Admin adjustment", + Timestamp: time.Now().UnixMilli(), + } + content, _ := giftLog.Marshal() + // Add gift amount change log + err = l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + Type: log.TypeGift.Uint8(), + Date: time.Now().Format(time.DateOnly), + ObjectID: userInfo.Id, + Content: string(content), + }) + if err != nil { + l.Errorw("[UpdateUserBasicInfoLogic] Insert Balance Log Error:", logger.Field("err", err.Error()), logger.Field("userId", req.UserId)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "Insert Balance Log Error") + } + userInfo.GiftAmount = req.GiftAmount + } + } + + if req.Commission != userInfo.Commission { + + commentLog := log.Commission{ + Type: log.CommissionTypeAdjust, + Amount: req.Commission - userInfo.Commission, + Timestamp: time.Now().UnixMilli(), + } + + content, _ := commentLog.Marshal() + err = l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + Type: log.TypeCommission.Uint8(), + Date: time.Now().Format(time.DateOnly), + ObjectID: userInfo.Id, + Content: string(content), + }) + if err != nil { + l.Errorw("[UpdateUserBasicInfoLogic] Insert Commission Log Error:", logger.Field("err", err.Error()), logger.Field("userId", req.UserId)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "Insert Commission Log Error") + } + userInfo.Commission = req.Commission + } + tool.DeepCopy(userInfo, req) + userInfo.OnlyFirstPurchase = &req.OnlyFirstPurchase + userInfo.ReferralPercentage = req.ReferralPercentage if req.Password != "" { if userInfo.Id == 2 && isDemo { return errors.Wrapf(xerr.NewErrCodeMsg(503, "Demo mode does not allow modification of the admin user password"), "UpdateUserBasicInfo failed: cannot update admin user password in demo mode") } userInfo.Password = tool.EncodePassWord(req.Password) + userInfo.Algo = "default" } err = l.svcCtx.UserModel.Update(l.ctx, userInfo) diff --git a/internal/logic/admin/user/updateUserSubscribeLogic.go b/internal/logic/admin/user/updateUserSubscribeLogic.go index 4b50771..9d92ce5 100644 --- a/internal/logic/admin/user/updateUserSubscribeLogic.go +++ b/internal/logic/admin/user/updateUserSubscribeLogic.go @@ -28,7 +28,7 @@ func NewUpdateUserSubscribeLogic(ctx context.Context, svcCtx *svc.ServiceContext } func (l *UpdateUserSubscribeLogic) UpdateUserSubscribe(req *types.UpdateUserSubscribeRequest) error { - userSub, err := l.svcCtx.UserModel.FindOneUserSubscribe(l.ctx, req.UserSubscribeId) + userSub, err := l.svcCtx.UserModel.FindOneSubscribe(l.ctx, req.UserSubscribeId) if err != nil { 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()) @@ -59,6 +59,15 @@ func (l *UpdateUserSubscribeLogic) UpdateUserSubscribe(req *types.UpdateUserSubs l.Errorw("UpdateSubscribe failed:", logger.Field("error", err.Error())) return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "UpdateSubscribe failed: %v", err.Error()) } - + // Clear user subscribe cache + if err = l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, userSub); err != nil { + l.Errorw("ClearSubscribeCache failed:", logger.Field("error", err.Error()), logger.Field("userSubscribeId", userSub.Id)) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "ClearSubscribeCache failed: %v", err.Error()) + } + // Clear subscribe cache + if err = l.svcCtx.SubscribeModel.ClearCache(l.ctx, userSub.SubscribeId); err != nil { + l.Errorw("failed to clear subscribe cache", logger.Field("error", err.Error()), logger.Field("subscribeId", userSub.SubscribeId)) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "failed to clear subscribe cache: %v", err.Error()) + } return nil } diff --git a/internal/logic/app/announcement/queryAnnouncementLogic.go b/internal/logic/app/announcement/queryAnnouncementLogic.go deleted file mode 100644 index 72be867..0000000 --- a/internal/logic/app/announcement/queryAnnouncementLogic.go +++ /dev/null @@ -1,47 +0,0 @@ -package announcement - -import ( - "context" - - "github.com/perfect-panel/server/internal/model/announcement" - "github.com/perfect-panel/server/pkg/tool" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" - - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" -) - -type QueryAnnouncementLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// NewQueryAnnouncementLogic Query announcement -func NewQueryAnnouncementLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryAnnouncementLogic { - return &QueryAnnouncementLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *QueryAnnouncementLogic) QueryAnnouncement(req *types.QueryAnnouncementRequest) (resp *types.QueryAnnouncementResponse, err error) { - enable := true - total, list, err := l.svcCtx.AnnouncementModel.GetAnnouncementListByPage(l.ctx, req.Page, req.Size, announcement.Filter{ - Show: &enable, - Pinned: req.Pinned, - Popup: req.Popup, - }) - if err != nil { - l.Error("[QueryAnnouncementLogic] GetAnnouncementListByPage error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetAnnouncementListByPage error: %v", err.Error()) - } - resp = &types.QueryAnnouncementResponse{} - resp.Total = total - resp.List = make([]types.Announcement, 0) - tool.DeepCopy(&resp.List, list) - return -} diff --git a/internal/logic/app/auth/checkLogic.go b/internal/logic/app/auth/checkLogic.go deleted file mode 100644 index 8114b96..0000000 --- a/internal/logic/app/auth/checkLogic.go +++ /dev/null @@ -1,41 +0,0 @@ -package auth - -import ( - "context" - - "github.com/pkg/errors" - "gorm.io/gorm" - - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" -) - -type CheckLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Check Account -func NewCheckLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CheckLogic { - return &CheckLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *CheckLogic) Check(req *types.AppAuthCheckRequest) (resp *types.AppAuthCheckResponse, err error) { - resp = &types.AppAuthCheckResponse{} - _, err = findUserByMethod(l.ctx, l.svcCtx, req.Method, req.Identifier, req.Account, req.AreaCode) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - resp.Status = false - return resp, nil - } - return resp, err - } - resp.Status = true - return -} diff --git a/internal/logic/app/auth/findUserByMethod.go b/internal/logic/app/auth/findUserByMethod.go deleted file mode 100644 index edc3dcb..0000000 --- a/internal/logic/app/auth/findUserByMethod.go +++ /dev/null @@ -1,59 +0,0 @@ -package auth - -import ( - "context" - - "github.com/perfect-panel/server/pkg/authmethod" - - "github.com/perfect-panel/server/internal/model/user" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/pkg/phone" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" - "gorm.io/gorm" -) - -func findUserByMethod(ctx context.Context, svcCtx *svc.ServiceContext, method, identifier, account, areaCode string) (userInfo *user.User, err error) { - var authMethods *user.AuthMethods - switch method { - case authmethod.Email: - authMethods, err = svcCtx.UserModel.FindUserAuthMethodByOpenID(ctx, authmethod.Email, account) - case authmethod.Mobile: - phoneNumber, err := phone.FormatToE164(areaCode, account) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.TelephoneError), "Invalid phone number") - } - authMethods, err = svcCtx.UserModel.FindUserAuthMethodByOpenID(ctx, authmethod.Mobile, phoneNumber) - if err != nil { - return nil, err - } - case authmethod.Device: - userDevice, err := svcCtx.UserModel.FindOneDeviceByIdentifier(ctx, identifier) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, err - } - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user device imei error") - } - return svcCtx.UserModel.FindOne(ctx, userDevice.UserId) - default: - return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "unknown method") - } - if err != nil { - return nil, err - } - return svcCtx.UserModel.FindOne(ctx, authMethods.UserId) -} - -func existError(method string) error { - switch method { - case authmethod.Email: - return errors.Wrapf(xerr.NewErrCode(xerr.EmailExist), "") - case authmethod.Mobile: - return errors.Wrapf(xerr.NewErrCode(xerr.TelephoneExist), "") - case authmethod.Device: - return errors.Wrapf(xerr.NewErrCode(xerr.DeviceExist), "") - default: - return errors.New("unknown method") - } -} diff --git a/internal/logic/app/auth/getAppConfigLogic.go b/internal/logic/app/auth/getAppConfigLogic.go deleted file mode 100644 index 26ce7f2..0000000 --- a/internal/logic/app/auth/getAppConfigLogic.go +++ /dev/null @@ -1,119 +0,0 @@ -package auth - -import ( - "context" - "encoding/json" - "fmt" - "strings" - - "github.com/perfect-panel/server/internal/model/application" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" - "gorm.io/gorm" - - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" -) - -type GetAppConfigLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// GetAppConfig -func NewGetAppConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAppConfigLogic { - return &GetAppConfigLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *GetAppConfigLogic) GetAppConfig(req *types.AppConfigRequest) (resp *types.AppConfigResponse, err error) { - resp = &types.AppConfigResponse{} - systems, err := l.svcCtx.SystemModel.GetSiteConfig(l.ctx) - if err != nil { - l.Errorw("[QueryApplicationConfig] GetSiteConfig error: ", logger.Field("error", err.Error())) - } - for _, sysVal := range systems { - if sysVal.Key == "CustomData" { - jsonStr := strings.ReplaceAll(sysVal.Value, "\\", "") - customData := make(map[string]interface{}) - if err = json.Unmarshal([]byte(jsonStr), &customData); err != nil { - break - } - - website := customData["website"] - if website != nil { - resp.OfficialWebsite = fmt.Sprintf("%v", website) - } - - contacts := customData["contacts"] - if contacts != nil { - contactsJson, err := json.Marshal(contacts) - if err == nil { - contactsMap := make(map[string]string) - err = json.Unmarshal(contactsJson, &contactsMap) - if err == nil { - resp.OfficialEmail = fmt.Sprintf("%v", contactsMap["email"]) - resp.OfficialTelegram = fmt.Sprintf("%v", contactsMap["telegram"]) - resp.OfficialTelephone = fmt.Sprintf("%v", contactsMap["telephone"]) - } - } - } - break - } - } - - var applications []*application.Application - err = l.svcCtx.ApplicationModel.Transaction(l.ctx, func(tx *gorm.DB) (err error) { - return tx.Model(applications).Preload("ApplicationVersions").Find(&applications).Error - }) - if err != nil { - l.Errorw("[QueryApplicationConfig] get application error: ", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get application error: %v", err.Error()) - } - - if len(applications) == 0 { - return resp, nil - } - - isOk := false - for _, app := range applications { - if isOk { - break - } - resp.Application.Name = app.Name - resp.Application.Description = app.Description - applicationVersions := app.ApplicationVersions - if len(applicationVersions) != 0 { - for _, applicationVersion := range applicationVersions { - if applicationVersion.Platform == req.UserAgent { - resp.Application.Id = applicationVersion.ApplicationId - resp.Application.Url = applicationVersion.Url - resp.Application.Version = applicationVersion.Version - resp.Application.VersionDescription = applicationVersion.Description - resp.Application.IsDefault = applicationVersion.IsDefault - isOk = true - break - } - } - } - } - - configs, err := l.svcCtx.ApplicationModel.FindOneConfig(l.ctx, 1) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - l.Logger.Error("[GetAppInfo] FindOneAppConfig error: ", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetAppInfo FindOneAppConfig error: %v", err.Error()) - } - resp.EncryptionKey = configs.EncryptionKey - resp.EncryptionMethod = configs.EncryptionMethod - resp.Domains = strings.Split(configs.Domains, ";") - resp.StartupPicture = configs.StartupPicture - resp.StartupPictureSkipTime = configs.StartupPictureSkipTime - resp.InvitationLink = configs.InvitationLink - resp.KrWebsiteId = configs.KrWebsiteId - return -} diff --git a/internal/logic/app/auth/loginLogic.go b/internal/logic/app/auth/loginLogic.go deleted file mode 100644 index 14b0751..0000000 --- a/internal/logic/app/auth/loginLogic.go +++ /dev/null @@ -1,194 +0,0 @@ -package auth - -import ( - "encoding/json" - "fmt" - "time" - - "github.com/perfect-panel/server/pkg/authmethod" - - "github.com/gin-gonic/gin" - - "github.com/perfect-panel/server/pkg/constant" - "github.com/perfect-panel/server/pkg/phone" - - "github.com/perfect-panel/server/internal/config" - "github.com/perfect-panel/server/internal/logic/common" - "github.com/perfect-panel/server/internal/model/user" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/jwt" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/tool" - "github.com/perfect-panel/server/pkg/uuidx" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" - "gorm.io/gorm" -) - -type LoginLogic struct { - logger.Logger - ctx *gin.Context - svcCtx *svc.ServiceContext -} - -// Login -func NewLoginLogic(ctx *gin.Context, svcCtx *svc.ServiceContext) *LoginLogic { - return &LoginLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *LoginLogic) Login(req *types.AppAuthRequest) (resp *types.AppAuthRespone, err error) { - - loginStatus := false - var userInfo *user.User - // Record login status - defer func(svcCtx *svc.ServiceContext) { - if userInfo != nil && userInfo.Id != 0 { - if err := svcCtx.UserModel.InsertLoginLog(l.ctx, &user.LoginLog{ - UserId: userInfo.Id, - LoginIP: l.ctx.ClientIP(), - UserAgent: l.ctx.Request.UserAgent(), - Success: &loginStatus, - }); err != nil { - l.Errorw("InsertLoginLog Error", logger.Field("error", err.Error())) - } - } - }(l.svcCtx) - - resp = &types.AppAuthRespone{} - //query user - userInfo, err = findUserByMethod(l.ctx, l.svcCtx, req.Method, req.Identifier, req.Account, req.AreaCode) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserPasswordError), "user password") - } - return resp, err - } - - switch req.Method { - case authmethod.Email: - - if !l.svcCtx.Config.Email.Enable { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.EmailNotEnabled), "Email function is not enabled yet") - } - - if req.Code != "" { - cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, constant.Security.String(), req.Account) - value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() - if err != nil { - l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - - var payload common.CacheKeyPayload - err = json.Unmarshal([]byte(value), &payload) - if err != nil { - l.Errorw("Unmarshal Error", logger.Field("error", err.Error()), logger.Field("value", value)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - - if payload.Code != req.Code { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - l.svcCtx.Redis.Del(l.ctx, cacheKey) - } else { - // Verify password - if !tool.VerifyPassWord(req.Password, userInfo.Password) { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserPasswordError), "user password") - } - } - case authmethod.Mobile: - if !l.svcCtx.Config.Mobile.Enable { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SmsNotEnabled), "sms login is not enabled") - } - phoneNumber, err := phone.FormatToE164(req.AreaCode, req.Account) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.TelephoneError), "Invalid phone number") - } - - if req.Code != "" { - cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeTelephoneCacheKey, constant.Security, phoneNumber) - value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() - if err != nil { - l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - - if value == "" { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - - var payload common.CacheKeyPayload - if err := json.Unmarshal([]byte(value), &payload); err != nil { - l.Errorw("[SendSmsCode]: Unmarshal Error", logger.Field("error", err.Error()), logger.Field("value", value)) - } - if payload.Code != req.Code { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - l.svcCtx.Redis.Del(l.ctx, cacheKey) - } else { - // Verify password - if !tool.VerifyPassWord(req.Password, userInfo.Password) { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserPasswordError), "user password") - } - } - case authmethod.Device: - default: - return nil, existError(req.Method) - } - - device, err := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - if req.Method == authmethod.Device { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "device not exist") - } - //Add User Device - userInfo.UserDevices = append(userInfo.UserDevices, user.Device{ - UserAgent: req.UserAgent, - Identifier: req.Identifier, - Ip: l.ctx.ClientIP(), - }) - err = l.svcCtx.UserModel.Update(l.ctx, userInfo) - if err != nil { - l.Errorw("[UpdateUserBindDevice] Fail", logger.Field("error", err.Error())) - } - } - } else { - //Change the user who owns the device - if device.UserId != userInfo.Id { - device.UserId = userInfo.Id - } - device.Ip = l.ctx.ClientIP() - err = l.svcCtx.UserModel.UpdateDevice(l.ctx, device) - if err != nil { - l.Errorw("[UpdateUserBindDevice] Fail", logger.Field("error", err.Error())) - } - } - - // Generate session id - sessionId := uuidx.NewUUID().String() - // Generate token - token, err := jwt.NewJwtToken( - l.svcCtx.Config.JwtAuth.AccessSecret, - time.Now().Unix(), - l.svcCtx.Config.JwtAuth.AccessExpire, - jwt.WithOption("UserId", userInfo.Id), - jwt.WithOption("SessionId", sessionId), - ) - if err != nil { - l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "token generate error: %v", err.Error()) - } - sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId) - if err = l.svcCtx.Redis.Set(l.ctx, sessionIdCacheKey, userInfo.Id, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err.Error()) - } - - resp.Token = token - return -} diff --git a/internal/logic/app/auth/registerLogic.go b/internal/logic/app/auth/registerLogic.go deleted file mode 100644 index 5d43ed4..0000000 --- a/internal/logic/app/auth/registerLogic.go +++ /dev/null @@ -1,249 +0,0 @@ -package auth - -import ( - "encoding/json" - "fmt" - "time" - - "github.com/perfect-panel/server/pkg/authmethod" - - "github.com/gin-gonic/gin" - - "github.com/perfect-panel/server/internal/config" - "github.com/perfect-panel/server/internal/logic/common" - "github.com/perfect-panel/server/internal/model/user" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/constant" - "github.com/perfect-panel/server/pkg/jwt" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/phone" - "github.com/perfect-panel/server/pkg/tool" - "github.com/perfect-panel/server/pkg/uuidx" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" - "gorm.io/gorm" -) - -type CacheKeyPayload struct { - Code string `json:"code"` - LastAt int64 `json:"lastAt"` -} -type RegisterLogic struct { - logger.Logger - ctx *gin.Context - svcCtx *svc.ServiceContext -} - -// Register -func NewRegisterLogic(ctx *gin.Context, svcCtx *svc.ServiceContext) *RegisterLogic { - return &RegisterLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *RegisterLogic) Register(req *types.AppAuthRequest) (resp *types.AppAuthRespone, err error) { - resp = &types.AppAuthRespone{} - var referer *user.User - c := l.svcCtx.Config.Register - // Check if the registration is stopped - if c.StopRegister { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.StopRegister), "stop register") - } - - if req.Invite == "" { - if l.svcCtx.Config.Invite.ForcedInvite { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.InviteCodeError), "invite code is required") - } - } else { - // Check if the invite code is valid - referer, err = l.svcCtx.UserModel.FindOneByReferCode(l.ctx, req.Invite) - if err != nil { - l.Errorw("FindOneByReferCode Error", logger.Field("error", err)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.InviteCodeError), "invite code is invalid") - } - } - - if req.Password == "" { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.PasswordIsEmpty), "Password required") - } - - userInfo, err := findUserByMethod(l.ctx, l.svcCtx, req.Method, req.Identifier, req.Account, req.AreaCode) - if err == nil && userInfo != nil { - return nil, existError(req.Method) - } - if !errors.Is(err, gorm.ErrRecordNotFound) { - return nil, err - } - // Generate password - pwd := tool.EncodePassWord(req.Password) - userInfo = &user.User{ - Password: pwd, - } - if referer != nil { - userInfo.RefererId = referer.Id - } - switch req.Method { - case authmethod.Email: - if !l.svcCtx.Config.Email.Enable { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.EmailNotEnabled), "Email function is not enabled yet") - } - if l.svcCtx.Config.Email.EnableVerify { - cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, constant.Register.String(), req.Account) - value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() - if err != nil { - l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - - var payload common.CacheKeyPayload - err = json.Unmarshal([]byte(value), &payload) - if err != nil { - l.Errorw("Unmarshal Error", logger.Field("error", err.Error()), logger.Field("value", value)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - - if payload.Code != req.Code { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - } - userInfo.AuthMethods = []user.AuthMethods{{ - AuthType: authmethod.Email, - AuthIdentifier: req.Account, - }} - - case authmethod.Mobile: - if req.AreaCode == "" { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.TelephoneAreaCodeIsEmpty), "area code required") - } - - if !l.svcCtx.Config.Mobile.Enable { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SmsNotEnabled), "sms login is not enabled") - } - phoneNumber, err := phone.FormatToE164(req.AreaCode, req.Account) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.TelephoneError), "Invalid phone number") - } - cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeTelephoneCacheKey, constant.Register, phoneNumber) - value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() - if err != nil { - l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - - var payload CacheKeyPayload - _ = json.Unmarshal([]byte(value), &payload) - if payload.Code != req.Code { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - userInfo.AuthMethods = []user.AuthMethods{{ - AuthType: authmethod.Mobile, - AuthIdentifier: phoneNumber, - Verified: true, - }} - case authmethod.Device: - oneDevice, err := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier) - if err == nil && oneDevice != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DeviceExist), "device exist") - } - default: - return nil, existError(req.Method) - } - - device, err := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - //Add User Device - userInfo.UserDevices = append(userInfo.UserDevices, user.Device{ - Ip: l.ctx.ClientIP(), - Identifier: req.Identifier, - UserAgent: req.UserAgent, - }) - } else { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user info failed: %v", err.Error()) - } - } else { - //Delete Other User Device - err = l.svcCtx.UserModel.DeleteDevice(l.ctx, device.Id) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "delete old user device failed: %v", err.Error()) - } else { - //User Add Device - userInfo.UserDevices = append(userInfo.UserDevices, user.Device{ - Ip: l.ctx.ClientIP(), - Identifier: req.Identifier, - UserAgent: req.UserAgent, - }) - } - } - - err = l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { - // Save user information - if err := db.Create(userInfo).Error; err != nil { - return err - } - // Generate ReferCode - userInfo.ReferCode = uuidx.UserInviteCode(userInfo.Id) - // Update ReferCode - if err := db.Model(&user.User{}).Where("id = ?", userInfo.Id).Update("refer_code", userInfo.ReferCode).Error; err != nil { - return err - } - if l.svcCtx.Config.Register.EnableTrial { - // Active trial - if err = l.activeTrial(userInfo.Id); err != nil { - return err - } - } - return nil - }) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert user info failed: %v", err.Error()) - } - - // Generate session id - sessionId := uuidx.NewUUID().String() - // Generate token - token, err := jwt.NewJwtToken( - l.svcCtx.Config.JwtAuth.AccessSecret, - time.Now().Unix(), - l.svcCtx.Config.JwtAuth.AccessExpire, - jwt.WithOption("UserId", userInfo.Id), - jwt.WithOption("SessionId", sessionId), - ) - if err != nil { - l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "token generate error: %v", err.Error()) - } - - sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId) - if err := l.svcCtx.Redis.Set(l.ctx, sessionIdCacheKey, userInfo.Id, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err.Error()) - } - - resp.Token = token - return -} - -func (l *RegisterLogic) activeTrial(uid int64) error { - sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, l.svcCtx.Config.Register.TrialSubscribe) - if err != nil { - return err - } - userSub := &user.Subscribe{ - Id: 0, - UserId: uid, - OrderId: 0, - SubscribeId: sub.Id, - StartTime: time.Now(), - ExpireTime: tool.AddTime(l.svcCtx.Config.Register.TrialTimeUnit, l.svcCtx.Config.Register.TrialTime, time.Now()), - Traffic: sub.Traffic, - Download: 0, - Upload: 0, - Token: uuidx.SubscribeToken(fmt.Sprintf("Trial-%v", uid)), - UUID: uuidx.NewUUID().String(), - Status: 1, - } - return l.svcCtx.UserModel.InsertSubscribe(l.ctx, userSub) -} diff --git a/internal/logic/app/auth/resetPasswordLogic.go b/internal/logic/app/auth/resetPasswordLogic.go deleted file mode 100644 index 1b3feee..0000000 --- a/internal/logic/app/auth/resetPasswordLogic.go +++ /dev/null @@ -1,161 +0,0 @@ -package auth - -import ( - "encoding/json" - "fmt" - "time" - - "github.com/perfect-panel/server/pkg/authmethod" - - "github.com/gin-gonic/gin" - - "github.com/perfect-panel/server/pkg/constant" - "github.com/perfect-panel/server/pkg/phone" - - "github.com/perfect-panel/server/internal/config" - "github.com/perfect-panel/server/internal/logic/common" - "github.com/perfect-panel/server/internal/model/user" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/jwt" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/tool" - "github.com/perfect-panel/server/pkg/uuidx" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" - "gorm.io/gorm" -) - -type ResetPasswordLogic struct { - logger.Logger - ctx *gin.Context - svcCtx *svc.ServiceContext -} - -// Reset Password -func NewResetPasswordLogic(ctx *gin.Context, svcCtx *svc.ServiceContext) *ResetPasswordLogic { - return &ResetPasswordLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *ResetPasswordLogic) ResetPassword(req *types.AppAuthRequest) (resp *types.AppAuthRespone, err error) { - resp = &types.AppAuthRespone{} - userInfo, err := findUserByMethod(l.ctx, l.svcCtx, req.Method, req.Identifier, req.Account, req.AreaCode) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "query user info failed") - } - l.Errorw("FindOneByEmail Error", logger.Field("error", err)) - return nil, err - } - - switch req.Method { - case authmethod.Mobile: - if !l.svcCtx.Config.Mobile.Enable { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SmsNotEnabled), "sms login is not enabled") - } - phoneNumber, err := phone.FormatToE164(req.AreaCode, req.Account) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.TelephoneError), "Invalid phone number") - } - cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeTelephoneCacheKey, constant.Security, phoneNumber) - value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() - if err != nil { - l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - - var payload common.CacheKeyPayload - err = json.Unmarshal([]byte(value), &payload) - if err != nil { - l.Errorw("Unmarshal Error", logger.Field("error", err.Error()), logger.Field("value", value)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - - if payload.Code != req.Code { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - case authmethod.Email: - if !l.svcCtx.Config.Email.Enable { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.EmailNotEnabled), "Email function is not enabled yet") - } - - cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, constant.Security.String(), req.Account) - value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() - if err != nil { - l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - - var payload CacheKeyPayload - err = json.Unmarshal([]byte(value), &payload) - if err != nil { - l.Errorw("Unmarshal Error", logger.Field("error", err.Error()), logger.Field("value", value)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - - if payload.Code != req.Code { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - default: - return nil, errors.New("unknown method") - } - - userInfo.Password = tool.EncodePassWord(req.Password) - err = l.svcCtx.UserModel.Update(l.ctx, userInfo) - if err != nil { - l.Errorw("UpdateUser Error", logger.Field("error", err)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update user info failed: %v", err.Error()) - } - - device, err := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - //Add User Device - userInfo.UserDevices = append(userInfo.UserDevices, user.Device{ - Ip: l.ctx.ClientIP(), - Identifier: req.Identifier, - UserAgent: req.UserAgent, - }) - } else { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user info failed: %v", err.Error()) - } - } else { - if device.UserId != userInfo.Id { - //Change the user who owns the device - if device.UserId != userInfo.Id { - device.UserId = userInfo.Id - } - device.Ip = l.ctx.ClientIP() - err = l.svcCtx.UserModel.UpdateDevice(l.ctx, device) - if err != nil { - l.Errorw("[UpdateUserBindDevice] Fail", logger.Field("error", err.Error())) - } - } - } - - // Generate session id - sessionId := uuidx.NewUUID().String() - // Generate token - token, err := jwt.NewJwtToken( - l.svcCtx.Config.JwtAuth.AccessSecret, - time.Now().Unix(), - l.svcCtx.Config.JwtAuth.AccessExpire, - jwt.WithOption("UserId", userInfo.Id), - jwt.WithOption("SessionId", sessionId), - ) - if err != nil { - l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "token generate error: %v", err.Error()) - } - - sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId) - if err := l.svcCtx.Redis.Set(l.ctx, sessionIdCacheKey, userInfo.Id, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err.Error()) - } - resp.Token = token - return -} diff --git a/internal/logic/app/document/queryDocumentDetailLogic.go b/internal/logic/app/document/queryDocumentDetailLogic.go deleted file mode 100644 index 4e033fa..0000000 --- a/internal/logic/app/document/queryDocumentDetailLogic.go +++ /dev/null @@ -1,39 +0,0 @@ -package document - -import ( - "context" - - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/tool" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" -) - -type QueryDocumentDetailLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// NewQueryDocumentDetailLogic Get document detail -func NewQueryDocumentDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryDocumentDetailLogic { - return &QueryDocumentDetailLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *QueryDocumentDetailLogic) QueryDocumentDetail(req *types.QueryDocumentDetailRequest) (resp *types.Document, err error) { - // find document - data, err := l.svcCtx.DocumentModel.FindOne(l.ctx, req.Id) - if err != nil { - l.Error("[QueryDocumentDetailLogic] FindOne error", logger.Field("id", req.Id), logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindOne error: %s", err.Error()) - } - resp = &types.Document{} - tool.DeepCopy(resp, data) - return -} diff --git a/internal/logic/app/document/queryDocumentListLogic.go b/internal/logic/app/document/queryDocumentListLogic.go deleted file mode 100644 index f42b266..0000000 --- a/internal/logic/app/document/queryDocumentListLogic.go +++ /dev/null @@ -1,48 +0,0 @@ -package document - -import ( - "context" - - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/tool" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" -) - -type QueryDocumentListLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Get document list -func NewQueryDocumentListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryDocumentListLogic { - return &QueryDocumentListLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *QueryDocumentListLogic) QueryDocumentList() (resp *types.QueryDocumentListResponse, err error) { - total, data, err := l.svcCtx.DocumentModel.GetDocumentListByAll(l.ctx) - if err != nil { - l.Error("[QueryDocumentList] error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "QueryDocumentList error: %v", err.Error()) - } - resp = &types.QueryDocumentListResponse{ - Total: total, - List: make([]types.Document, 0), - } - for _, item := range data { - resp.List = append(resp.List, types.Document{ - Id: item.Id, - Title: item.Title, - Tags: tool.StringMergeAndRemoveDuplicates(item.Tags), - UpdatedAt: item.UpdatedAt.UnixMilli(), - }) - } - return -} diff --git a/internal/logic/app/node/getNodeListLogic.go b/internal/logic/app/node/getNodeListLogic.go deleted file mode 100644 index df2420c..0000000 --- a/internal/logic/app/node/getNodeListLogic.go +++ /dev/null @@ -1,82 +0,0 @@ -package node - -import ( - "context" - "strconv" - "strings" - - "github.com/perfect-panel/server/pkg/constant" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" - - "github.com/perfect-panel/server/internal/model/user" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" -) - -type GetNodeListLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Get Node list -func NewGetNodeListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetNodeListLogic { - return &GetNodeListLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *GetNodeListLogic) GetNodeList(req *types.AppUserSubscbribeNodeRequest) (resp *types.AppUserSubscbribeNodeResponse, err error) { - resp = &types.AppUserSubscbribeNodeResponse{List: make([]types.AppUserSubscbribeNode, 0)} - userInfo := l.ctx.Value(constant.CtxKeyUser).(*user.User) - userSubscribe, err := l.svcCtx.UserModel.FindOneUserSubscribe(l.ctx, req.Id) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user subscribe: %v", err.Error()) - } - - if userInfo.Id != userSubscribe.UserId { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "find user subscribe: %v", err.Error()) - } - - //拿到所有订阅下的服务组id - var ids []int64 - for _, idStr := range strings.Split(userSubscribe.Subscribe.ServerGroup, ",") { - id, err := strconv.ParseInt(idStr, 10, 64) - if err != nil { - continue - } - ids = append(ids, id) - } - - //根据服务组id拿到所有节点 - servers, err := l.svcCtx.ServerModel.FindServerListByGroupIds(l.ctx, ids) - if err != nil { - return nil, err - } - for _, server := range servers { - resp.List = append(resp.List, types.AppUserSubscbribeNode{ - Id: server.Id, - Uuid: userSubscribe.UUID, - Traffic: userSubscribe.Traffic, - Upload: userSubscribe.Upload, - Download: userSubscribe.Download, - RelayNode: server.RelayNode, - RelayMode: server.RelayMode, - Longitude: server.Longitude, - Latitude: server.Latitude, - Tags: strings.Split(server.Tags, ","), - Config: server.Config, - ServerAddr: server.ServerAddr, - Protocol: server.Protocol, - SpeedLimit: server.SpeedLimit, - City: server.City, - Country: server.Country, - Name: server.Name, - }) - } - return -} diff --git a/internal/logic/app/node/getRuleGroupListLogic.go b/internal/logic/app/node/getRuleGroupListLogic.go deleted file mode 100644 index 64f1645..0000000 --- a/internal/logic/app/node/getRuleGroupListLogic.go +++ /dev/null @@ -1,41 +0,0 @@ -package node - -import ( - "context" - - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/tool" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" -) - -type GetRuleGroupListLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Get rule group list -func NewGetRuleGroupListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetRuleGroupListLogic { - return &GetRuleGroupListLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *GetRuleGroupListLogic) GetRuleGroupList() (resp *types.AppRuleGroupListResponse, err error) { - nodeRuleGroupList, err := l.svcCtx.ServerModel.QueryAllRuleGroup(l.ctx) - if err != nil { - l.Logger.Error("[GetRuleGroupList] get subscribe rule group list failed: ", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get subscribe rule group list failed: %v", err.Error()) - } - nodeRuleGroups := make([]types.ServerRuleGroup, 0) - tool.DeepCopy(&nodeRuleGroups, nodeRuleGroupList) - return &types.AppRuleGroupListResponse{ - Total: int64(len(nodeRuleGroups)), - List: nodeRuleGroups, - }, nil -} diff --git a/internal/logic/app/order/calculateCoupon.go b/internal/logic/app/order/calculateCoupon.go deleted file mode 100644 index 05f92c8..0000000 --- a/internal/logic/app/order/calculateCoupon.go +++ /dev/null @@ -1,13 +0,0 @@ -package order - -import ( - "github.com/perfect-panel/server/internal/model/coupon" -) - -func calculateCoupon(amount int64, couponInfo *coupon.Coupon) int64 { - if couponInfo.Type == 1 { - return int64(float64(amount) * (float64(couponInfo.Discount) / float64(100))) - } else { - return min(couponInfo.Discount, amount) - } -} diff --git a/internal/logic/app/order/calculateFee.go b/internal/logic/app/order/calculateFee.go deleted file mode 100644 index 9c0b2b9..0000000 --- a/internal/logic/app/order/calculateFee.go +++ /dev/null @@ -1,20 +0,0 @@ -package order - -import "github.com/perfect-panel/server/internal/model/payment" - -func calculateFee(amount int64, config *payment.Payment) int64 { - var fee float64 - switch config.FeeMode { - case 0: - return 0 - case 1: - fee = float64(amount) * (float64(config.FeePercent) / float64(100)) - case 2: - if amount > 0 { - fee = float64(config.FeeAmount) - } - case 3: - fee = float64(amount)*(float64(config.FeePercent)/float64(100)) + float64(config.FeeAmount) - } - return int64(fee) -} diff --git a/internal/logic/app/order/checkoutOrderLogic.go b/internal/logic/app/order/checkoutOrderLogic.go deleted file mode 100644 index 79fb745..0000000 --- a/internal/logic/app/order/checkoutOrderLogic.go +++ /dev/null @@ -1,325 +0,0 @@ -package order - -import ( - "context" - "encoding/json" - "fmt" - "strconv" - - paymentPlatform "github.com/perfect-panel/server/pkg/payment" - - "github.com/perfect-panel/server/pkg/constant" - - "github.com/hibiken/asynq" - "github.com/perfect-panel/server/internal/model/order" - "github.com/perfect-panel/server/internal/model/payment" - "github.com/perfect-panel/server/internal/model/user" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/exchangeRate" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/payment/alipay" - "github.com/perfect-panel/server/pkg/payment/epay" - "github.com/perfect-panel/server/pkg/payment/stripe" - "github.com/perfect-panel/server/pkg/tool" - "github.com/perfect-panel/server/pkg/xerr" - queueType "github.com/perfect-panel/server/queue/types" - "github.com/pkg/errors" - "gorm.io/gorm" -) - -type CheckoutOrderLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -type CurrencyConfig struct { - CurrencyUnit string - CurrencySymbol string - AccessKey string -} - -const ( - Stripe = "Stripe" - QR = "qr" - Link = "link" -) - -// NewCheckoutOrderLogic Checkout order -func NewCheckoutOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CheckoutOrderLogic { - return &CheckoutOrderLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *CheckoutOrderLogic) CheckoutOrder(req *types.CheckoutOrderRequest, requestHost string) (resp *types.CheckoutOrderResponse, err error) { - u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) - if !ok { - l.Error("[CheckoutOrderLogic] Invalid access") - return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid access") - } - // find order - orderInfo, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo) - if err != nil { - l.Error("[CheckoutOrderLogic] FindOneByOrderNo error", logger.Field("orderNo", req.OrderNo), logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindOneByOrderNo error: %s", err.Error()) - } - - if orderInfo.Status != 1 { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Order status error") - } - - paymentConfig, err := l.svcCtx.PaymentModel.FindOne(l.ctx, orderInfo.PaymentId) - if err != nil { - l.Error("[CheckoutOrderLogic] FindOneByPaymentMark error", logger.Field("paymentMark", orderInfo.Method), logger.Field("PaymentID", orderInfo.PaymentId), logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindOneByPaymentMark error: %s", err.Error()) - } - var stripePayment *types.StripePayment = nil - var url, t string - - // switch payment method - switch paymentPlatform.ParsePlatform(paymentConfig.Platform) { - case paymentPlatform.Stripe: - result, err := l.stripePayment(paymentConfig.Config, orderInfo, u) - if err != nil { - l.Error("[CheckoutOrderLogic] stripePayment error", logger.Field("error", err.Error())) - return nil, err - } - stripePayment = result - t = Stripe - case paymentPlatform.EPay: - // epay - url, err = l.epayPayment(paymentConfig, orderInfo, req.ReturnUrl, requestHost) - if err != nil { - l.Error("[CheckoutOrderLogic] epayPayment error", logger.Field("error", err.Error())) - return nil, err - } - t = Link - case paymentPlatform.AlipayF2F: - // alipay f2f - url, err = l.alipayF2fPayment(paymentConfig, orderInfo, requestHost) - if err != nil { - return nil, err - } - t = QR - case paymentPlatform.Balance: - // balance - if err = l.balancePayment(u, orderInfo); err != nil { - return nil, err - } - t = paymentPlatform.Balance.String() - default: - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Payment method not supported") - } - return &types.CheckoutOrderResponse{ - Type: t, - CheckoutUrl: url, - Stripe: stripePayment, - }, nil -} - -// Query exchange rate -func (l *CheckoutOrderLogic) queryExchangeRate(to string, src int64) (amount float64, err error) { - amount = float64(src) / float64(100) - // query system currency - currency, err := l.svcCtx.SystemModel.GetCurrencyConfig(l.ctx) - if err != nil { - l.Error("[CheckoutOrderLogic] GetCurrencyConfig error", logger.Field("error", err.Error())) - return 0, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetCurrencyConfig error: %s", err.Error()) - } - configs := &CurrencyConfig{} - tool.SystemConfigSliceReflectToStruct(currency, configs) - if configs.AccessKey == "" { - return amount, nil - } - if configs.CurrencyUnit != to { - // query exchange rate - result, err := exchangeRate.GetExchangeRete(configs.CurrencyUnit, to, configs.AccessKey, 1) - if err != nil { - return 0, err - } - amount = result * amount - } - return amount, nil -} - -// Stripe Payment -func (l *CheckoutOrderLogic) stripePayment(config string, info *order.Order, u *user.User) (*types.StripePayment, error) { - // stripe WeChat pay or stripe alipay - stripeConfig := payment.StripeConfig{} - if err := json.Unmarshal([]byte(config), &stripeConfig); err != nil { - l.Error("[CheckoutOrderLogic] Unmarshal error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unmarshal error: %s", err.Error()) - } - client := stripe.NewClient(stripe.Config{ - SecretKey: stripeConfig.SecretKey, - PublicKey: stripeConfig.PublicKey, - WebhookSecret: stripeConfig.WebhookSecret, - }) - // Calculate the amount with exchange rate - amount, err := l.queryExchangeRate("CNY", info.Amount) - if err != nil { - l.Error("[CheckoutOrderLogic] queryExchangeRate error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "queryExchangeRate error: %s", err.Error()) - } - convertAmount := int64(amount * 100) - // create payment - result, err := client.CreatePaymentSheet(&stripe.Order{ - OrderNo: info.OrderNo, - Subscribe: strconv.FormatInt(info.SubscribeId, 10), - Amount: convertAmount, - Currency: "cny", - Payment: stripeConfig.Payment, - }, - &stripe.User{ - UserId: u.Id, - }) - if err != nil { - l.Error("[CheckoutOrderLogic] CreatePaymentSheet error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "CreatePaymentSheet error: %s", err.Error()) - } - tradeNo := result.TradeNo - stripePayment := &types.StripePayment{ - PublishableKey: stripeConfig.PublicKey, - ClientSecret: result.ClientSecret, - Method: stripeConfig.Payment, - } - // save payment - info.TradeNo = tradeNo - err = l.svcCtx.OrderModel.Update(l.ctx, info) - if err != nil { - l.Error("[CheckoutOrderLogic] Update error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Update error: %s", err.Error()) - } - return stripePayment, nil -} - -// epay payment -func (l *CheckoutOrderLogic) epayPayment(config *payment.Payment, info *order.Order, returnUrl, requestHost string) (string, error) { - epayConfig := payment.EPayConfig{} - if err := json.Unmarshal([]byte(config.Config), &epayConfig); err != nil { - l.Error("[CheckoutOrderLogic] Unmarshal error", logger.Field("error", err.Error())) - return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unmarshal error: %s", err.Error()) - } - client := epay.NewClient(epayConfig.Pid, epayConfig.Url, epayConfig.Key) - // Calculate the amount with exchange rate - amount, err := l.queryExchangeRate("CNY", info.Amount) - if err != nil { - return "", err - } - var domain string - if config.Domain != "" { - domain = config.Domain - } else { - domain = fmt.Sprintf("http://%s", requestHost) - } - // create payment - url := client.CreatePayUrl(epay.Order{ - Name: l.svcCtx.Config.Site.SiteName, - Amount: amount, - OrderNo: info.OrderNo, - SignType: "MD5", - NotifyUrl: domain + "/v1/notify/epay", - ReturnUrl: returnUrl, - }) - return url, nil -} - -// alipay f2f payment -func (l *CheckoutOrderLogic) alipayF2fPayment(pay *payment.Payment, info *order.Order, requestHost string) (string, error) { - f2FConfig := payment.AlipayF2FConfig{} - if err := json.Unmarshal([]byte(pay.Config), &f2FConfig); err != nil { - l.Error("[CheckoutOrderLogic] Unmarshal error", logger.Field("error", err.Error())) - return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unmarshal error: %s", err.Error()) - } - var domain string - if pay.Domain != "" { - domain = pay.Domain - } else { - domain = fmt.Sprintf("http://%s", requestHost) - } - client := alipay.NewClient(alipay.Config{ - AppId: f2FConfig.AppId, - PrivateKey: f2FConfig.PrivateKey, - PublicKey: f2FConfig.PublicKey, - InvoiceName: f2FConfig.InvoiceName, - NotifyURL: domain + "/notify/alipay", - }) - // Calculate the amount with exchange rate - amount, err := l.queryExchangeRate("CNY", info.Amount) - if err != nil { - l.Error("[CheckoutOrderLogic] queryExchangeRate error", logger.Field("error", err.Error())) - return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "queryExchangeRate error: %s", err.Error()) - } - convertAmount := int64(amount * 100) - // create payment - QRCode, err := client.PreCreateTrade(l.ctx, alipay.Order{ - OrderNo: info.OrderNo, - Amount: convertAmount, - }) - if err != nil { - l.Error("[CheckoutOrderLogic] PreCreateTrade error", logger.Field("error", err.Error())) - return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "PreCreateTrade error: %s", err.Error()) - } - return QRCode, nil -} - -// Balance payment -func (l *CheckoutOrderLogic) balancePayment(u *user.User, o *order.Order) error { - var userInfo user.User - err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { - err := db.Model(&user.User{}).Where("id = ?", u.Id).First(&userInfo).Error - if err != nil { - return err - } - - if userInfo.Balance < o.Amount { - return errors.Wrapf(xerr.NewErrCode(xerr.InsufficientBalance), "Insufficient balance") - } - // deduct balance - userInfo.Balance -= o.Amount - err = l.svcCtx.UserModel.Update(l.ctx, &userInfo) - if err != nil { - return err - } - // create balance log - balanceLog := &user.BalanceLog{ - Id: 0, - UserId: u.Id, - Amount: o.Amount, - Type: 3, - OrderId: o.Id, - Balance: userInfo.Balance, - } - err = db.Create(balanceLog).Error - if err != nil { - return err - } - return l.svcCtx.OrderModel.UpdateOrderStatus(l.ctx, o.OrderNo, 2) - }) - if err != nil { - l.Error("[CheckoutOrderLogic] Transaction error", logger.Field("error", err.Error()), logger.Field("orderNo", o.OrderNo)) - return err - } - // create activity order task - payload := queueType.ForthwithActivateOrderPayload{ - OrderNo: o.OrderNo, - } - bytes, err := json.Marshal(payload) - if err != nil { - l.Error("[CheckoutOrderLogic] Marshal error", logger.Field("error", err.Error())) - return err - } - - task := asynq.NewTask(queueType.ForthwithActivateOrder, bytes) - _, err = l.svcCtx.Queue.EnqueueContext(l.ctx, task) - if err != nil { - l.Error("[CheckoutOrderLogic] Enqueue error", logger.Field("error", err.Error())) - return err - } - l.Logger.Info("[CheckoutOrderLogic] Enqueue success", logger.Field("orderNo", o.OrderNo)) - return nil -} diff --git a/internal/logic/app/order/closeOrderLogic.go b/internal/logic/app/order/closeOrderLogic.go deleted file mode 100644 index 2bd1d44..0000000 --- a/internal/logic/app/order/closeOrderLogic.go +++ /dev/null @@ -1,186 +0,0 @@ -package order - -import ( - "context" - "encoding/json" - - paymentPlatform "github.com/perfect-panel/server/pkg/payment" - - "github.com/perfect-panel/server/internal/model/user" - "github.com/perfect-panel/server/pkg/payment/stripe" - "gorm.io/gorm" - - "github.com/perfect-panel/server/internal/model/order" - "github.com/perfect-panel/server/internal/model/payment" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/payment/alipay" -) - -type CloseOrderLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// NewCloseOrderLogic Close order -func NewCloseOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CloseOrderLogic { - return &CloseOrderLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *CloseOrderLogic) CloseOrder(req *types.CloseOrderRequest) error { - // Find order information by order number - orderInfo, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo) - if err != nil { - l.Error("[CloseOrder] Find order info failed", - logger.Field("error", err.Error()), - logger.Field("orderNo", req.OrderNo), - ) - return nil - } - // If the order status is not 1, it means that the order has been closed or paid - if orderInfo.Status != 1 { - l.Info("[CloseOrder] Order status is not 1", - logger.Field("orderNo", req.OrderNo), - logger.Field("status", orderInfo.Status), - ) - return nil - } - if l.confirmationPayment(orderInfo) { - l.Info("[CloseOrder] Order has been paid", - logger.Field("orderNo", req.OrderNo), - logger.Field("status", orderInfo.Status), - ) - return nil - } - err = l.svcCtx.DB.Transaction(func(tx *gorm.DB) error { - // update order status - err := tx.Model(&order.Order{}).Where("order_no = ?", req.OrderNo).Update("status", 3).Error - if err != nil { - l.Error("[CloseOrder] Update order status failed", - logger.Field("error", err.Error()), - logger.Field("orderNo", req.OrderNo), - ) - return err - } - // refund deduction amount to user deduction balance - if orderInfo.GiftAmount > 0 { - userInfo, err := l.svcCtx.UserModel.FindOne(l.ctx, orderInfo.UserId) - if err != nil { - l.Error("[CloseOrder] Find user info failed", - logger.Field("error", err.Error()), - logger.Field("user_id", orderInfo.UserId), - ) - return err - } - deduction := userInfo.GiftAmount + orderInfo.GiftAmount - err = tx.Model(&user.User{}).Where("id = ?", orderInfo.UserId).Update("deduction", deduction).Error - if err != nil { - l.Error("[CloseOrder] Refund deduction amount failed", - logger.Field("error", err.Error()), - logger.Field("uid", orderInfo.UserId), - logger.Field("deduction", orderInfo.GiftAmount), - ) - return err - } - // Record the deduction refund log - giftAmountLog := &user.GiftAmountLog{ - UserId: orderInfo.UserId, - OrderNo: orderInfo.OrderNo, - Amount: orderInfo.GiftAmount, - Type: 1, - Balance: deduction, - Remark: "Order cancellation refund", - } - err = tx.Model(&user.GiftAmountLog{}).Create(giftAmountLog).Error - if err != nil { - l.Error("[CloseOrder] Record cancellation refund log failed", - logger.Field("error", err.Error()), - logger.Field("uid", orderInfo.UserId), - logger.Field("deduction", orderInfo.GiftAmount), - ) - return err - } - // update user cache - return l.svcCtx.UserModel.UpdateUserCache(l.ctx, userInfo) - } - return nil - }) - if err != nil { - return err - } - return nil -} - -// confirmationPayment Determine whether the payment is successful -// -//nolint:unused -func (l *CloseOrderLogic) confirmationPayment(order *order.Order) bool { - paymentConfig, err := l.svcCtx.PaymentModel.FindOne(l.ctx, order.PaymentId) - if err != nil { - l.Error("[CloseOrder] Find payment config failed", logger.Field("error", err.Error()), logger.Field("paymentMark", order.Method)) - return false - } - switch paymentPlatform.ParsePlatform(order.Method) { - case paymentPlatform.AlipayF2F: - if l.queryAlipay(paymentConfig, order.TradeNo) { - return true - } - case paymentPlatform.Stripe: - if l.queryStripe(paymentConfig, order.TradeNo) { - return true - } - default: - l.Info("[CloseOrder] Unsupported payment method", logger.Field("paymentMethod", order.Method)) - } - return false -} - -// queryAlipay Query Alipay payment status -func (l *CloseOrderLogic) queryAlipay(paymentConfig *payment.Payment, TradeNo string) bool { - config := payment.AlipayF2FConfig{} - if err := json.Unmarshal([]byte(paymentConfig.Config), &config); err != nil { - l.Error("[CloseOrder] Unmarshal payment config failed", logger.Field("error", err.Error()), logger.Field("config", paymentConfig.Config)) - return false - } - client := alipay.NewClient(alipay.Config{ - AppId: config.AppId, - PrivateKey: config.PrivateKey, - PublicKey: config.PublicKey, - InvoiceName: config.InvoiceName, - }) - status, err := client.QueryTrade(l.ctx, TradeNo) - if err != nil { - l.Error("[CloseOrder] Query trade failed", logger.Field("error", err.Error()), logger.Field("TradeNo", TradeNo)) - return false - } - if status == alipay.Success || status == alipay.Finished { - return true - } - return false -} - -// queryStripe Query Stripe payment status -func (l *CloseOrderLogic) queryStripe(paymentConfig *payment.Payment, TradeNo string) bool { - config := payment.StripeConfig{} - if err := json.Unmarshal([]byte(paymentConfig.Config), &config); err != nil { - l.Error("[CloseOrder] Unmarshal payment config failed", logger.Field("error", err.Error()), logger.Field("config", paymentConfig.Config)) - return false - } - client := stripe.NewClient(stripe.Config{ - PublicKey: config.PublicKey, - SecretKey: config.SecretKey, - WebhookSecret: config.WebhookSecret, - }) - status, err := client.QueryOrderStatus(TradeNo) - if err != nil { - l.Error("[CloseOrder] Query order status failed", logger.Field("error", err.Error()), logger.Field("TradeNo", TradeNo)) - return false - } - return status -} diff --git a/internal/logic/app/order/getDiscount.go b/internal/logic/app/order/getDiscount.go deleted file mode 100644 index c645e89..0000000 --- a/internal/logic/app/order/getDiscount.go +++ /dev/null @@ -1,14 +0,0 @@ -package order - -import "github.com/perfect-panel/server/internal/types" - -func getDiscount(discounts []types.SubscribeDiscount, inputMonths int64) float64 { - var finalDiscount int64 = 100 - - for _, discount := range discounts { - if inputMonths >= discount.Quantity && discount.Discount < finalDiscount { - finalDiscount = discount.Discount - } - } - return float64(finalDiscount) / float64(100) -} diff --git a/internal/logic/app/order/preCreateOrderLogic.go b/internal/logic/app/order/preCreateOrderLogic.go deleted file mode 100644 index 1f72559..0000000 --- a/internal/logic/app/order/preCreateOrderLogic.go +++ /dev/null @@ -1,123 +0,0 @@ -package order - -import ( - "context" - "encoding/json" - - "github.com/perfect-panel/server/internal/model/order" - "github.com/perfect-panel/server/pkg/tool" - - "github.com/perfect-panel/server/pkg/constant" - - "github.com/perfect-panel/server/internal/model/user" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" - "gorm.io/gorm" -) - -type PreCreateOrderLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Pre create order -func NewPreCreateOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PreCreateOrderLogic { - return &PreCreateOrderLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (resp *types.PreOrderResponse, err error) { - u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) - if !ok { - logger.Error("current user is not found in context") - return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") - } - // find subscribe plan - sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, req.SubscribeId) - if err != nil { - l.Error("[PreCreateOrder] Database query error", logger.Field("error", err.Error()), logger.Field("subscribe_id", req.SubscribeId)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe error: %v", err.Error()) - } - var discount float64 = 1 - if sub.Discount != "" { - var dis []types.SubscribeDiscount - _ = json.Unmarshal([]byte(sub.Discount), &dis) - discount = getDiscount(dis, req.Quantity) - } - price := sub.UnitPrice * req.Quantity - amount := int64(float64(price) * discount) - discountAmount := price - amount - var coupon int64 - if req.Coupon != "" { - couponInfo, err := l.svcCtx.CouponModel.FindOneByCode(l.ctx, req.Coupon) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponNotExist), "coupon not found") - } - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find coupon error: %v", err.Error()) - } - if couponInfo.Count > 0 && couponInfo.Count <= couponInfo.UsedCount { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponInsufficientUsage), "coupon used") - } - couponSub := tool.StringToInt64Slice(couponInfo.Subscribe) - if len(couponSub) > 0 && !tool.Contains(couponSub, req.SubscribeId) { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponNotApplicable), "coupon not match") - } - var count int64 - err = l.svcCtx.DB.Transaction(func(tx *gorm.DB) error { - return tx.Model(&order.Order{}).Where("user_id = ? and coupon = ?", u.Id, req.Coupon).Count(&count).Error - }) - - if err != nil { - l.Errorw("[Purchase] 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()) - } - if couponInfo.UserLimit > 0 && count >= couponInfo.UserLimit { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponInsufficientUsage), "coupon limit exceeded") - } - coupon = calculateCoupon(amount, couponInfo) - } - amount -= coupon - - var deductionAmount int64 - // Check user deduction amount - if u.GiftAmount > 0 { - if u.GiftAmount >= amount { - deductionAmount = amount - amount = 0 - } else { - deductionAmount = u.GiftAmount - amount -= u.GiftAmount - } - } - - payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment) - if err != nil { - l.Logger.Error("[PreCreateOrder] Database query error", logger.Field("error", err.Error()), logger.Field("payment", req.Payment)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find payment method error: %v", err.Error()) - } - var feeAmount int64 - // Calculate the handling fee - if amount > 0 { - feeAmount = calculateFee(amount, payment) - } - amount += feeAmount - - resp = &types.PreOrderResponse{ - Price: price, - Amount: amount, - Discount: discountAmount, - GiftAmount: deductionAmount, - Coupon: req.Coupon, - CouponDiscount: coupon, - FeeAmount: feeAmount, - } - return -} diff --git a/internal/logic/app/order/purchaseLogic.go b/internal/logic/app/order/purchaseLogic.go deleted file mode 100644 index 04a7992..0000000 --- a/internal/logic/app/order/purchaseLogic.go +++ /dev/null @@ -1,214 +0,0 @@ -package order - -import ( - "context" - "encoding/json" - "time" - - "github.com/perfect-panel/server/pkg/constant" - - "github.com/hibiken/asynq" - "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/internal/types" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/tool" - "github.com/perfect-panel/server/pkg/xerr" - queue "github.com/perfect-panel/server/queue/types" - "github.com/pkg/errors" - "gorm.io/gorm" -) - -type PurchaseLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -const CloseOrderTimeMinutes = 15 - -// purchase Subscription -func NewPurchaseLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PurchaseLogic { - return &PurchaseLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.PurchaseOrderResponse, err error) { - - u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) - if !ok { - logger.Error("current user is not found in context") - return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") - } - // find user subscription - - if l.svcCtx.Config.Subscribe.SingleModel { - userSub, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, u.Id) - if err != nil { - l.Error("[Purchase] Database query error", logger.Field("error", err.Error()), logger.Field("user_id", u.Id)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user subscription error: %v", err.Error()) - } - if len(userSub) > 0 { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserSubscribeExist), "user has subscription") - } - } - - // find subscribe plan - sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, req.SubscribeId) - - if err != nil { - l.Error("[Purchase] Database query error", logger.Field("error", err.Error()), logger.Field("subscribe_id", req.SubscribeId)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe error: %v", err.Error()) - } - // check subscribe plan status - if !*sub.Sell { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "subscribe not sell") - } - var discount float64 = 1 - if sub.Discount != "" { - var dis []types.SubscribeDiscount - _ = json.Unmarshal([]byte(sub.Discount), &dis) - discount = getDiscount(dis, req.Quantity) - } - price := sub.UnitPrice * req.Quantity - // discount amount - amount := int64(float64(price) * discount) - discountAmount := price - amount - var coupon int64 = 0 - // Calculate the coupon deduction - if req.Coupon != "" { - couponInfo, err := l.svcCtx.CouponModel.FindOneByCode(l.ctx, req.Coupon) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponNotExist), "coupon not found") - } - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find coupon error: %v", err.Error()) - } - if couponInfo.Count <= couponInfo.UsedCount { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponInsufficientUsage), "coupon used") - } - couponSub := tool.StringToInt64Slice(couponInfo.Subscribe) - if len(couponSub) > 0 && !tool.Contains(couponSub, req.SubscribeId) { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponNotApplicable), "coupon not match") - } - var count int64 - err = l.svcCtx.DB.Transaction(func(tx *gorm.DB) error { - return tx.Model(&order.Order{}).Where("user_id = ? and coupon = ?", u.Id, req.Coupon).Count(&count).Error - }) - - if err != nil { - l.Errorw("[Purchase] 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()) - } - if count >= couponInfo.UserLimit { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponInsufficientUsage), "coupon limit exceeded") - } - coupon = calculateCoupon(amount, couponInfo) - } - // Calculate the handling fee - amount -= coupon - var deductionAmount int64 - // Check user deduction amount - if u.GiftAmount > 0 { - if u.GiftAmount >= amount { - deductionAmount = amount - amount = 0 - u.GiftAmount -= amount - } else { - deductionAmount = u.GiftAmount - amount -= u.GiftAmount - u.GiftAmount = 0 - } - } - // find payment method - payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment) - if err != nil { - l.Logger.Error("[Purchase] Database query error", logger.Field("error", err.Error()), logger.Field("payment", req.Payment)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find payment method error: %v", err.Error()) - } - var feeAmount int64 - // Calculate the handling fee - if amount > 0 { - feeAmount = calculateFee(amount, payment) - } - // query user is new purchase or renewal - isNew, err := l.svcCtx.OrderModel.IsUserEligibleForNewOrder(l.ctx, u.Id) - if err != nil { - l.Error("[Purchase] Database query error", logger.Field("error", err.Error()), logger.Field("user_id", u.Id)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user order error: %v", err.Error()) - } - // create order - orderInfo := &order.Order{ - UserId: u.Id, - OrderNo: tool.GenerateTradeNo(), - Type: 1, - Quantity: req.Quantity, - Price: price, - Amount: amount, - Discount: discountAmount, - GiftAmount: deductionAmount, - Coupon: req.Coupon, - CouponDiscount: coupon, - PaymentId: req.Payment, - Method: payment.Platform, - FeeAmount: feeAmount, - Status: 1, - IsNew: isNew, - SubscribeId: req.SubscribeId, - } - // Database transaction - err = l.svcCtx.DB.Transaction(func(db *gorm.DB) error { - // update user deduction && Pre deduction ,Return after canceling the order - if orderInfo.GiftAmount > 0 { - // update user deduction && Pre deduction ,Return after canceling the order - if e := l.svcCtx.UserModel.Update(l.ctx, u, db); err != nil { - l.Error("[Purchase] Database update error", logger.Field("error", err.Error()), logger.Field("user", u)) - return e - } - // create deduction record - giftAmountLog := user.GiftAmountLog{ - UserId: orderInfo.UserId, - OrderNo: orderInfo.OrderNo, - Amount: orderInfo.GiftAmount, - Type: 2, - Balance: u.GiftAmount, - Remark: "Purchase order deduction", - } - if e := db.Model(&user.GiftAmountLog{}).Create(&giftAmountLog).Error; e != nil { - l.Error("[Purchase] Database insert error", - logger.Field("error", err.Error()), - logger.Field("deductionLog", giftAmountLog), - ) - return e - } - } - // insert order - return db.Model(&order.Order{}).Create(&orderInfo).Error - }) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert order error: %v", err.Error()) - } - // Deferred task - payload := queue.DeferCloseOrderPayload{ - OrderNo: orderInfo.OrderNo, - } - val, err := json.Marshal(payload) - if err != nil { - l.Error("[CreateOrder] Marshal payload error", logger.Field("error", err.Error()), logger.Field("payload", payload)) - } - task := asynq.NewTask(queue.DeferCloseOrder, val, asynq.MaxRetry(3)) - taskInfo, err := l.svcCtx.Queue.Enqueue(task, asynq.ProcessIn(CloseOrderTimeMinutes*time.Minute)) - if err != nil { - l.Error("[CreateOrder] Enqueue task error", logger.Field("error", err.Error()), logger.Field("task", task)) - } else { - l.Info("[CreateOrder] Enqueue task success", logger.Field("TaskID", taskInfo.ID)) - } - - return &types.PurchaseOrderResponse{ - OrderNo: orderInfo.OrderNo, - }, nil -} diff --git a/internal/logic/app/order/queryOrderDetailLogic.go b/internal/logic/app/order/queryOrderDetailLogic.go deleted file mode 100644 index 8938445..0000000 --- a/internal/logic/app/order/queryOrderDetailLogic.go +++ /dev/null @@ -1,40 +0,0 @@ -package order - -import ( - "context" - - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/tool" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" -) - -type QueryOrderDetailLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Get order -func NewQueryOrderDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryOrderDetailLogic { - return &QueryOrderDetailLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *QueryOrderDetailLogic) QueryOrderDetail(req *types.QueryOrderDetailRequest) (resp *types.OrderDetail, err error) { - orderInfo, err := l.svcCtx.OrderModel.FindOneDetailsByOrderNo(l.ctx, req.OrderNo) - if err != nil { - l.Error("[QueryOrderDetail] Database query error", logger.Field("error", err.Error()), logger.Field("order_no", req.OrderNo)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find order error: %v", err.Error()) - } - resp = &types.OrderDetail{} - tool.DeepCopy(resp, orderInfo) - // Prevent commission amount leakage - resp.Commission = 0 - return -} diff --git a/internal/logic/app/order/queryOrderListLogic.go b/internal/logic/app/order/queryOrderListLogic.go deleted file mode 100644 index 275fda3..0000000 --- a/internal/logic/app/order/queryOrderListLogic.go +++ /dev/null @@ -1,56 +0,0 @@ -package order - -import ( - "context" - - "github.com/perfect-panel/server/pkg/constant" - - "github.com/perfect-panel/server/internal/model/user" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/tool" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" -) - -type QueryOrderListLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Get order list -func NewQueryOrderListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryOrderListLogic { - return &QueryOrderListLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *QueryOrderListLogic) QueryOrderList(req *types.QueryOrderListRequest) (resp *types.QueryOrderListResponse, err error) { - u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) - if !ok { - logger.Error("current user is not found in context") - return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") - } - total, data, err := l.svcCtx.OrderModel.QueryOrderListByPage(l.ctx, req.Page, req.Size, 0, u.Id, 0, "") - if err != nil { - l.Error("[QueryOrderListLogic] Query order list failed", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query order list failed") - } - resp = &types.QueryOrderListResponse{ - Total: total, - List: make([]types.OrderDetail, 0), - } - for _, item := range data { - var orderInfo types.OrderDetail - tool.DeepCopy(&orderInfo, item) - // Prevent commission amount leakage - orderInfo.Commission = 0 - resp.List = append(resp.List, orderInfo) - } - - return -} diff --git a/internal/logic/app/order/rechargeLogic.go b/internal/logic/app/order/rechargeLogic.go deleted file mode 100644 index 7e805b8..0000000 --- a/internal/logic/app/order/rechargeLogic.go +++ /dev/null @@ -1,92 +0,0 @@ -package order - -import ( - "context" - "encoding/json" - "time" - - "github.com/perfect-panel/server/pkg/constant" - "github.com/perfect-panel/server/pkg/xerr" - - "github.com/hibiken/asynq" - "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/internal/types" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/tool" - queue "github.com/perfect-panel/server/queue/types" - "github.com/pkg/errors" -) - -type RechargeLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// NewRechargeLogic Recharge -func NewRechargeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RechargeLogic { - return &RechargeLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *RechargeLogic) Recharge(req *types.RechargeOrderRequest) (resp *types.RechargeOrderResponse, err error) { - u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) - if !ok { - logger.Error("current user is not found in context") - return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") - } - // find payment method - payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment) - if err != nil { - l.Error("[Recharge] Database query error", logger.Field("error", err.Error()), logger.Field("payment", req.Payment)) - return nil, errors.Wrapf(err, "find payment error: %v", err.Error()) - } - // Calculate the handling fee - feeAmount := calculateFee(req.Amount, payment) - // query user is new purchase or renewal - isNew, err := l.svcCtx.OrderModel.IsUserEligibleForNewOrder(l.ctx, u.Id) - if err != nil { - l.Error("[Recharge] Database query error", logger.Field("error", err.Error()), logger.Field("user_id", u.Id)) - return nil, errors.Wrapf(err, "query user error: %v", err.Error()) - } - orderInfo := order.Order{ - UserId: u.Id, - OrderNo: tool.GenerateTradeNo(), - Type: 4, - Price: req.Amount, - Amount: req.Amount + feeAmount, - FeeAmount: feeAmount, - PaymentId: req.Payment, - Method: payment.Platform, - Status: 1, - IsNew: isNew, - } - err = l.svcCtx.OrderModel.Insert(l.ctx, &orderInfo) - if err != nil { - l.Error("[Recharge] Database insert error", logger.Field("error", err.Error()), logger.Field("order", orderInfo)) - return nil, errors.Wrapf(err, "insert order error: %v", err.Error()) - } - // Deferred task - payload := queue.DeferCloseOrderPayload{ - OrderNo: orderInfo.OrderNo, - } - val, err := json.Marshal(payload) - if err != nil { - l.Error("[Recharge] Marshal payload error", logger.Field("error", err.Error()), logger.Field("payload", payload)) - } - task := asynq.NewTask(queue.DeferCloseOrder, val, asynq.MaxRetry(3)) - taskInfo, err := l.svcCtx.Queue.Enqueue(task, asynq.ProcessIn(CloseOrderTimeMinutes*time.Minute)) - if err != nil { - l.Error("[Recharge] Enqueue task error", logger.Field("error", err.Error()), logger.Field("task", task)) - } else { - l.Info("[Recharge] Enqueue task success", logger.Field("TaskID", taskInfo.ID)) - } - return &types.RechargeOrderResponse{ - OrderNo: orderInfo.OrderNo, - }, nil -} diff --git a/internal/logic/app/order/renewalLogic.go b/internal/logic/app/order/renewalLogic.go deleted file mode 100644 index 5571a66..0000000 --- a/internal/logic/app/order/renewalLogic.go +++ /dev/null @@ -1,178 +0,0 @@ -package order - -import ( - "context" - "encoding/json" - "time" - - "github.com/perfect-panel/server/pkg/constant" - - "github.com/hibiken/asynq" - "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/internal/types" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/tool" - "github.com/perfect-panel/server/pkg/xerr" - queue "github.com/perfect-panel/server/queue/types" - "github.com/pkg/errors" - "gorm.io/gorm" -) - -type RenewalLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Renewal Subscription -func NewRenewalLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RenewalLogic { - return &RenewalLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.RenewalOrderResponse, err error) { - u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) - if !ok { - logger.Error("current user is not found in context") - return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") - } - orderNo := tool.GenerateTradeNo() - // find user subscribe - userSubscribe, err := l.svcCtx.UserModel.FindOneUserSubscribe(l.ctx, req.UserSubscribeID) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user subscribe error: %v", err.Error()) - } - // find subscription - sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, userSubscribe.SubscribeId) - if err != nil { - l.Error("[Renewal] Database query error", logger.Field("error", err.Error()), logger.Field("subscribe_id", userSubscribe.SubscribeId)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe error: %v", err.Error()) - } - // check subscribe plan status - if !*sub.Sell { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "subscribe not sell") - } - var discount float64 = 1 - if sub.Discount != "" { - var dis []types.SubscribeDiscount - _ = json.Unmarshal([]byte(sub.Discount), &dis) - discount = getDiscount(dis, req.Quantity) - } - price := sub.UnitPrice * req.Quantity - amount := int64(float64(price) * discount) - discountAmount := price - amount - var coupon int64 = 0 - if req.Coupon != "" { - couponInfo, err := l.svcCtx.CouponModel.FindOneByCode(l.ctx, req.Coupon) - if err != nil { - l.Error("[Renewal] Database query error", logger.Field("error", err.Error()), logger.Field("coupon", req.Coupon)) - return nil, errors.Wrapf(err, "find coupon error: %v", err.Error()) - } - if couponInfo.Count <= couponInfo.UsedCount { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponInsufficientUsage), "coupon used") - } - coupon = calculateCoupon(amount, couponInfo) - } - payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment) - if err != nil { - l.Error("[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()) - } - amount -= coupon - - var deductionAmount int64 - // Check user deduction amount - if u.GiftAmount > 0 { - if u.GiftAmount >= amount { - deductionAmount = amount - amount = 0 - u.GiftAmount -= amount - } else { - deductionAmount = u.GiftAmount - amount -= u.GiftAmount - u.GiftAmount = 0 - } - } - - var feeAmount int64 - // Calculate the handling fee - if amount > 0 { - feeAmount = calculateFee(amount, payment) - } - - amount += feeAmount - - // create order - orderInfo := order.Order{ - UserId: u.Id, - ParentId: userSubscribe.OrderId, - OrderNo: orderNo, - Type: 2, - Quantity: req.Quantity, - Price: price, - Amount: amount, - GiftAmount: deductionAmount, - Discount: discountAmount, - Coupon: req.Coupon, - CouponDiscount: coupon, - PaymentId: payment.Id, - Method: payment.Platform, - FeeAmount: feeAmount, - Status: 1, - SubscribeId: userSubscribe.SubscribeId, - SubscribeToken: userSubscribe.Token, - } - // Database transaction - err = l.svcCtx.DB.Transaction(func(db *gorm.DB) error { - // update user deduction && Pre deduction ,Return after canceling the order - if orderInfo.GiftAmount > 0 { - // update user deduction && Pre deduction ,Return after canceling the order - if err := l.svcCtx.UserModel.Update(l.ctx, u, db); err != nil { - l.Error("[Purchase] Database update error", logger.Field("error", err.Error()), logger.Field("user", u)) - return err - } - // create deduction record - deductionLog := user.GiftAmountLog{ - UserId: orderInfo.UserId, - OrderNo: orderInfo.OrderNo, - Amount: orderInfo.GiftAmount, - Type: 2, - Balance: u.GiftAmount, - Remark: "Renewal order deduction", - } - if err := db.Model(&user.GiftAmountLog{}).Create(&deductionLog).Error; err != nil { - l.Error("[Renewal] Database insert error", logger.Field("error", err.Error()), logger.Field("deductionLog", deductionLog)) - return err - } - } - // insert order - return db.Model(&order.Order{}).Create(&orderInfo).Error - }) - if err != nil { - l.Error("[Renewal] Database insert error", logger.Field("error", err.Error()), logger.Field("order", orderInfo)) - return nil, errors.Wrapf(err, "insert order error: %v", err.Error()) - } - // Deferred task - payload := queue.DeferCloseOrderPayload{ - OrderNo: orderInfo.OrderNo, - } - val, err := json.Marshal(payload) - if err != nil { - l.Error("[Renewal] Marshal payload error", logger.Field("error", err.Error()), logger.Field("payload", payload)) - } - task := asynq.NewTask(queue.DeferCloseOrder, val, asynq.MaxRetry(3)) - taskInfo, err := l.svcCtx.Queue.Enqueue(task, asynq.ProcessIn(CloseOrderTimeMinutes*time.Minute)) - if err != nil { - l.Error("[Renewal] Enqueue task error", logger.Field("error", err.Error()), logger.Field("task", task)) - } else { - l.Info("[Renewal] Enqueue task success", logger.Field("TaskID", taskInfo.ID)) - } - return &types.RenewalOrderResponse{ - OrderNo: orderInfo.OrderNo, - }, nil -} diff --git a/internal/logic/app/order/resetTrafficLogic.go b/internal/logic/app/order/resetTrafficLogic.go deleted file mode 100644 index 9d1f60f..0000000 --- a/internal/logic/app/order/resetTrafficLogic.go +++ /dev/null @@ -1,146 +0,0 @@ -package order - -import ( - "context" - "encoding/json" - "time" - - "github.com/perfect-panel/server/pkg/constant" - "github.com/perfect-panel/server/pkg/xerr" - - "gorm.io/gorm" - - "github.com/hibiken/asynq" - "github.com/perfect-panel/server/internal/model/order" - "github.com/perfect-panel/server/internal/model/user" - "github.com/perfect-panel/server/pkg/tool" - queue "github.com/perfect-panel/server/queue/types" - "github.com/pkg/errors" - - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" -) - -type ResetTrafficLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Reset traffic -func NewResetTrafficLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ResetTrafficLogic { - return &ResetTrafficLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *ResetTrafficLogic) ResetTraffic(req *types.ResetTrafficOrderRequest) (resp *types.ResetTrafficOrderResponse, err error) { - u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) - if !ok { - logger.Error("current user is not found in context") - return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") - } - // find user subscription - userSubscribe, err := l.svcCtx.UserModel.FindOneUserSubscribe(l.ctx, req.UserSubscribeID) - if err != nil { - l.Error("[ResetTraffic] Database query error", logger.Field("error", err.Error()), logger.Field("UserSubscribeID", req.UserSubscribeID)) - return nil, errors.Wrapf(err, "find user subscribe error: %v", err.Error()) - } - if userSubscribe.Subscribe == nil { - l.Error("[ResetTraffic] subscribe not found", logger.Field("UserSubscribeID", req.UserSubscribeID)) - return nil, errors.New("subscribe not found") - } - amount := userSubscribe.Subscribe.Replacement - var deductionAmount int64 - // Check user deduction amount - if u.GiftAmount > 0 { - if u.GiftAmount >= amount { - deductionAmount = amount - amount = 0 - u.GiftAmount -= amount - } else { - deductionAmount = u.GiftAmount - amount -= u.GiftAmount - u.GiftAmount = 0 - } - } - // find payment method - payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment) - if err != nil { - l.Error("[ResetTraffic] Database query error", logger.Field("error", err.Error()), logger.Field("payment", req.Payment)) - return nil, errors.Wrapf(err, "find payment error: %v", err.Error()) - } - var feeAmount int64 - // Calculate the handling fee - if amount > 0 { - feeAmount = calculateFee(amount, payment) - } - // create order - orderInfo := order.Order{ - Id: 0, - ParentId: userSubscribe.OrderId, - UserId: u.Id, - OrderNo: tool.GenerateTradeNo(), - Type: 3, - Price: userSubscribe.Subscribe.Replacement, - Amount: amount + feeAmount, - GiftAmount: deductionAmount, - FeeAmount: feeAmount, - PaymentId: req.Payment, - Method: payment.Platform, - Status: 1, - SubscribeId: userSubscribe.SubscribeId, - SubscribeToken: userSubscribe.Token, - } - // Database transaction - err = l.svcCtx.DB.Transaction(func(db *gorm.DB) error { - // update user deduction && Pre deduction ,Return after canceling the order - if orderInfo.GiftAmount > 0 { - // update user deduction && Pre deduction ,Return after canceling the order - if err := l.svcCtx.UserModel.Update(l.ctx, u, db); err != nil { - l.Error("[ResetTraffic] Database update error", logger.Field("error", err.Error()), logger.Field("user", u)) - return err - } - // create deduction record - deductionLog := user.GiftAmountLog{ - UserId: orderInfo.UserId, - OrderNo: orderInfo.OrderNo, - Amount: orderInfo.GiftAmount, - Type: 2, - Balance: u.GiftAmount, - Remark: "ResetTraffic order deduction", - } - if err := db.Model(&user.GiftAmountLog{}).Create(&deductionLog).Error; err != nil { - l.Error("[ResetTraffic] Database insert error", logger.Field("error", err.Error()), logger.Field("deductionLog", deductionLog)) - return err - } - } - // insert order - return db.Model(&order.Order{}).Create(&orderInfo).Error - }) - if err != nil { - l.Error("[ResetTraffic] Database insert error", logger.Field("error", err.Error()), logger.Field("order", orderInfo)) - return nil, errors.Wrapf(err, "insert order error: %v", err.Error()) - } - // Deferred task - payload := queue.DeferCloseOrderPayload{ - OrderNo: orderInfo.OrderNo, - } - val, err := json.Marshal(payload) - if err != nil { - l.Error("[ResetTraffic] Marshal payload error", logger.Field("error", err.Error()), logger.Field("payload", payload)) - } - task := asynq.NewTask(queue.DeferCloseOrder, val, asynq.MaxRetry(3)) - taskInfo, err := l.svcCtx.Queue.Enqueue(task, asynq.ProcessIn(CloseOrderTimeMinutes*time.Minute)) - if err != nil { - l.Error("[ResetTraffic] Enqueue task error", logger.Field("error", err.Error()), logger.Field("task", task)) - } else { - l.Info("[ResetTraffic] Enqueue task success", logger.Field("TaskID", taskInfo.ID)) - } - return &types.ResetTrafficOrderResponse{ - OrderNo: orderInfo.OrderNo, - }, nil -} diff --git a/internal/logic/app/payment/getAvailablePaymentMethodsLogic.go b/internal/logic/app/payment/getAvailablePaymentMethodsLogic.go deleted file mode 100644 index 79ea7e0..0000000 --- a/internal/logic/app/payment/getAvailablePaymentMethodsLogic.go +++ /dev/null @@ -1,40 +0,0 @@ -package payment - -import ( - "context" - - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/tool" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" -) - -type GetAvailablePaymentMethodsLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// NewGetAvailablePaymentMethodsLogic Get available payment methods -func NewGetAvailablePaymentMethodsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAvailablePaymentMethodsLogic { - return &GetAvailablePaymentMethodsLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *GetAvailablePaymentMethodsLogic) GetAvailablePaymentMethods() (resp *types.GetAvailablePaymentMethodsResponse, err error) { - data, err := l.svcCtx.PaymentModel.FindAvailableMethods(l.ctx) - if err != nil { - l.Error("[GetAvailablePaymentMethods] database error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetAvailablePaymentMethods: %v", err.Error()) - } - resp = &types.GetAvailablePaymentMethodsResponse{ - List: make([]types.PaymentMethod, 0), - } - tool.DeepCopy(&resp.List, data) - return -} diff --git a/internal/logic/app/subscribe/queryApplicationConfigLogic.go b/internal/logic/app/subscribe/queryApplicationConfigLogic.go deleted file mode 100644 index a76337f..0000000 --- a/internal/logic/app/subscribe/queryApplicationConfigLogic.go +++ /dev/null @@ -1,115 +0,0 @@ -package subscribe - -import ( - "context" - - "github.com/perfect-panel/server/internal/model/application" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" - "gorm.io/gorm" - - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" -) - -type QueryApplicationConfigLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Get application config -func NewQueryApplicationConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryApplicationConfigLogic { - return &QueryApplicationConfigLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *QueryApplicationConfigLogic) QueryApplicationConfig() (resp *types.ApplicationResponse, err error) { - resp = &types.ApplicationResponse{} - var applications []*application.Application - err = l.svcCtx.ApplicationModel.Transaction(l.ctx, func(tx *gorm.DB) (err error) { - return tx.Model(applications).Preload("ApplicationVersions").Find(&applications).Error - }) - if err != nil { - l.Errorw("[QueryApplicationConfig] get application error: ", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get application error: %v", err.Error()) - } - - if len(applications) == 0 { - return resp, nil - } - - for _, app := range applications { - applicationResponse := types.ApplicationResponseInfo{ - Id: app.Id, - Name: app.Name, - Icon: app.Icon, - Description: app.Description, - SubscribeType: app.SubscribeType, - } - applicationVersions := app.ApplicationVersions - if len(applicationVersions) != 0 { - for _, applicationVersion := range applicationVersions { - /*if !applicationVersion.IsDefault { - continue - }*/ - switch applicationVersion.Platform { - case "ios": - applicationResponse.Platform.IOS = append(applicationResponse.Platform.IOS, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - case "macos": - applicationResponse.Platform.MacOS = append(applicationResponse.Platform.MacOS, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - case "linux": - applicationResponse.Platform.Linux = append(applicationResponse.Platform.Linux, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - case "android": - applicationResponse.Platform.Android = append(applicationResponse.Platform.Android, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - case "windows": - applicationResponse.Platform.Windows = append(applicationResponse.Platform.Windows, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - case "harmony": - applicationResponse.Platform.Harmony = append(applicationResponse.Platform.Harmony, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - } - } - } - resp.Applications = append(resp.Applications, applicationResponse) - } - return -} diff --git a/internal/logic/app/subscribe/querySubscribeGroupListLogic.go b/internal/logic/app/subscribe/querySubscribeGroupListLogic.go deleted file mode 100644 index 0f7c947..0000000 --- a/internal/logic/app/subscribe/querySubscribeGroupListLogic.go +++ /dev/null @@ -1,44 +0,0 @@ -package subscribe - -import ( - "context" - - "github.com/perfect-panel/server/internal/model/subscribe" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/tool" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" -) - -type QuerySubscribeGroupListLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Get subscribe group list -func NewQuerySubscribeGroupListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QuerySubscribeGroupListLogic { - return &QuerySubscribeGroupListLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *QuerySubscribeGroupListLogic) QuerySubscribeGroupList() (resp *types.QuerySubscribeGroupListResponse, err error) { - var list []*subscribe.Group - var total int64 - err = l.svcCtx.DB.Model(&subscribe.Group{}).Count(&total).Find(&list).Error - if err != nil { - l.Logger.Error("[QuerySubscribeGroupListLogic] get subscribe group list failed: ", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get subscribe group list failed: %v", err.Error()) - } - groupList := make([]types.SubscribeGroup, 0) - tool.DeepCopy(&groupList, list) - return &types.QuerySubscribeGroupListResponse{ - Total: total, - List: groupList, - }, nil -} diff --git a/internal/logic/app/subscribe/querySubscribeListLogic.go b/internal/logic/app/subscribe/querySubscribeListLogic.go deleted file mode 100644 index 474376a..0000000 --- a/internal/logic/app/subscribe/querySubscribeListLogic.go +++ /dev/null @@ -1,55 +0,0 @@ -package subscribe - -import ( - "context" - "encoding/json" - - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/tool" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" -) - -type QuerySubscribeListLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Get subscribe list -func NewQuerySubscribeListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QuerySubscribeListLogic { - return &QuerySubscribeListLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *QuerySubscribeListLogic) QuerySubscribeList() (resp *types.QuerySubscribeListResponse, err error) { - - data, err := l.svcCtx.SubscribeModel.QuerySubscribeList(l.ctx) - if err != nil { - l.Errorw("[QuerySubscribeListLogic] Database Error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "QuerySubscribeList error: %v", err.Error()) - } - resp = &types.QuerySubscribeListResponse{ - List: make([]types.Subscribe, 0), - Total: int64(len(data)), - } - for _, v := range data { - var sub types.Subscribe - tool.DeepCopy(&sub, v) - if v.Discount != "" { - if err = json.Unmarshal([]byte(v.Discount), &sub.Discount); err != nil { - l.Errorw("[QuerySubscribeListLogic] json.Unmarshal Error", logger.Field("error", err.Error()), logger.Field("value", v.Discount)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "json.Unmarshal error: %v", err.Error()) - } - } else { - sub.Discount = make([]types.SubscribeDiscount, 0) - } - resp.List = append(resp.List, sub) - } - return -} diff --git a/internal/logic/app/subscribe/queryUserAlreadySubscribeLogic.go b/internal/logic/app/subscribe/queryUserAlreadySubscribeLogic.go deleted file mode 100644 index b25e110..0000000 --- a/internal/logic/app/subscribe/queryUserAlreadySubscribeLogic.go +++ /dev/null @@ -1,67 +0,0 @@ -package subscribe - -import ( - "context" - - "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/internal/types" - "github.com/perfect-panel/server/pkg/constant" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" - "gorm.io/gorm" -) - -type QueryUserAlreadySubscribeLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Get Already subscribed to package -func NewQueryUserAlreadySubscribeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryUserAlreadySubscribeLogic { - return &QueryUserAlreadySubscribeLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *QueryUserAlreadySubscribeLogic) QueryUserAlreadySubscribe() (resp *types.QueryUserSubscribeResp, err error) { - resp = &types.QueryUserSubscribeResp{ - Data: make([]types.UserSubscribeData, 0), - } - userInfo := l.ctx.Value(constant.CtxKeyUser).(*user.User) - var orderIds []int64 - var subscribes []user.Subscribe - err = l.svcCtx.OrderModel.Transaction(context.Background(), func(tx *gorm.DB) error { - if err := tx.Model(&order.Order{}).Where("user_id = ? AND status in ?", userInfo.Id, []int64{2, 5}).Select("id").Find(&orderIds).Error; err != nil { - return err - } - if len(orderIds) == 0 { - return nil - } - return tx.Model(&user.Subscribe{}).Where("user_id = ? AND order_id in ?", userInfo.Id, orderIds).Order("created_at desc").Find(&subscribes).Error - }) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find order error: %v", err.Error()) - } - if len(subscribes) == 0 { - return - } - - userAlreadySubscribe := make(map[int64]int64) - for _, subscribe := range subscribes { - userAlreadySubscribe[subscribe.SubscribeId] = subscribe.Id - } - - for k, v := range userAlreadySubscribe { - resp.Data = append(resp.Data, types.UserSubscribeData{ - SubscribeId: k, - UserSubscribeId: v, - }) - } - return -} diff --git a/internal/logic/app/subscribe/queryUserAvailableUserSubscribeLogic.go b/internal/logic/app/subscribe/queryUserAvailableUserSubscribeLogic.go deleted file mode 100644 index be67b99..0000000 --- a/internal/logic/app/subscribe/queryUserAvailableUserSubscribeLogic.go +++ /dev/null @@ -1,107 +0,0 @@ -package subscribe - -import ( - "context" - "strconv" - "strings" - "time" - - "github.com/perfect-panel/server/internal/model/user" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/constant" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" -) - -type QueryUserAvailableUserSubscribeLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Get Available subscriptions for users -func NewQueryUserAvailableUserSubscribeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryUserAvailableUserSubscribeLogic { - return &QueryUserAvailableUserSubscribeLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *QueryUserAvailableUserSubscribeLogic) QueryUserAvailableUserSubscribe(req *types.AppUserSubscribeRequest) (resp *types.AppUserSubscbribeResponse, err error) { - resp = &types.AppUserSubscbribeResponse{List: make([]types.AppUserSubcbribe, 0)} - userInfo := l.ctx.Value(constant.CtxKeyUser).(*user.User) - //查询用户订阅 - subscribeDetails, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, userInfo.Id, 1, 2) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get query user subscribe error: %v", err.Error()) - } - - userSubscribeMap := make(map[int64]types.AppUserSubcbribe) - for _, sd := range subscribeDetails { - userSubscribeInfo := types.AppUserSubcbribe{ - Id: sd.Id, - Name: sd.Subscribe.Name, - Traffic: sd.Traffic, - Upload: sd.Upload, - Download: sd.Download, - ExpireTime: sd.ExpireTime.Format(time.DateTime), - StartTime: sd.StartTime.Format(time.DateTime), - DeviceLimit: sd.Subscribe.DeviceLimit, - } - - //不需要查询节点 - if req.ContainsNodes == nil || !*req.ContainsNodes { - resp.List = append(resp.List, userSubscribeInfo) - continue - } - - //拿到所有订阅下的服务组id - var ids []int64 - for _, idStr := range strings.Split(sd.Subscribe.ServerGroup, ",") { - id, err := strconv.ParseInt(idStr, 10, 64) - if err != nil { - continue - } - ids = append(ids, id) - } - //根据服务组id拿到所有节点 - servers, err := l.svcCtx.ServerModel.FindServerListByGroupIds(l.ctx, ids) - if err != nil { - l.Logger.Errorf("FindServerListByGroupIds error: %v", err.Error()) - continue - } - - for _, server := range servers { - userSubscribeInfo.List = append(userSubscribeInfo.List, types.AppUserSubscbribeNode{ - Id: server.Id, - Uuid: sd.UUID, - Traffic: sd.Traffic, - Upload: sd.Upload, - Download: sd.Download, - RelayNode: server.RelayNode, - RelayMode: server.RelayMode, - Longitude: server.Longitude, - Latitude: server.Latitude, - Tags: strings.Split(server.Tags, ","), - Config: server.Config, - ServerAddr: server.ServerAddr, - Protocol: server.Protocol, - SpeedLimit: server.SpeedLimit, - City: server.City, - Country: server.Country, - Name: server.Name, - }) - } - resp.List = append(resp.List, userSubscribeInfo) - userSubscribeMap[userSubscribeInfo.Id] = userSubscribeInfo - } - - for _, userSubscribeInfo := range userSubscribeMap { - resp.List = append(resp.List, userSubscribeInfo) - } - return resp, nil - -} diff --git a/internal/logic/app/subscribe/resetUserSubscribePeriodLogic.go b/internal/logic/app/subscribe/resetUserSubscribePeriodLogic.go deleted file mode 100644 index edbc5fb..0000000 --- a/internal/logic/app/subscribe/resetUserSubscribePeriodLogic.go +++ /dev/null @@ -1,60 +0,0 @@ -package subscribe - -import ( - "context" - "time" - - "github.com/perfect-panel/server/internal/model/user" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/constant" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" -) - -type ResetUserSubscribePeriodLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewResetUserSubscribePeriodLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ResetUserSubscribePeriodLogic { - return &ResetUserSubscribePeriodLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *ResetUserSubscribePeriodLogic) ResetUserSubscribePeriod(req *types.UserSubscribeResetPeriodRequest) (resp *types.UserSubscribeResetPeriodResponse, err error) { - resp = &types.UserSubscribeResetPeriodResponse{} - userInfo := l.ctx.Value(constant.CtxKeyUser).(*user.User) - subscribe, err := l.svcCtx.UserModel.FindOneSubscribe(l.ctx, req.UserSubscribeId) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find order error: %v", err.Error()) - } - if userInfo.Id != subscribe.UserId { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SubscribeNotAvailable), "user not authorized,subscribe not available") - } - - if time.Now().After(subscribe.ExpireTime) { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SubscribeExpired), "subscribe expired") - } - - if subscribe.Traffic < 1 { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ExistAvailableTraffic), "Unlimited data plan.") - } - - if (subscribe.Download + subscribe.Upload + 10240) < subscribe.Traffic { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ExistAvailableTraffic), "There is still available traffic.") - } - - subscribe.ExpireTime = subscribe.ExpireTime.AddDate(0, -1, 0) - err = l.svcCtx.UserModel.UpdateSubscribe(l.ctx, subscribe) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update subscribe error: %v", err.Error()) - } - resp.Status = true - return -} diff --git a/internal/logic/app/user/deleteAccountLogic.go b/internal/logic/app/user/deleteAccountLogic.go deleted file mode 100644 index c666fb4..0000000 --- a/internal/logic/app/user/deleteAccountLogic.go +++ /dev/null @@ -1,103 +0,0 @@ -package user - -import ( - "context" - "encoding/json" - "fmt" - - "github.com/perfect-panel/server/internal/config" - "github.com/perfect-panel/server/internal/logic/common" - "github.com/perfect-panel/server/internal/model/user" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/constant" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" -) - -type DeleteAccountLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Delete Account -func NewDeleteAccountLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteAccountLogic { - return &DeleteAccountLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *DeleteAccountLogic) DeleteAccount(req *types.DeleteAccountRequest) error { - userInfo, exists := l.ctx.Value(constant.CtxKeyUser).(user.User) - if !exists { - return nil - } - - var account string - for _, authMethod := range userInfo.AuthMethods { - if authMethod.AuthType == req.Method { - account = authMethod.AuthIdentifier - break - } - } - if account == "" { - return nil - } - - if req.Method == "email" { - emailConfig := l.svcCtx.Config.Email - - if !emailConfig.Enable { - return errors.Wrapf(xerr.NewErrCode(xerr.EmailNotEnabled), "Email function is not enabled yet") - } - - if emailConfig.EnableVerify { - cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, constant.Security, account) - value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() - if err != nil { - l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey)) - return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - - var payload common.CacheKeyPayload - err = json.Unmarshal([]byte(value), &payload) - if err != nil { - l.Errorw("Unmarshal Error", logger.Field("error", err.Error()), logger.Field("value", value)) - return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - - if payload.Code != req.Code { - return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - } - } else { - cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeTelephoneCacheKey, constant.Security, account) - value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() - if err != nil { - l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey)) - return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - - if value == "" { - return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - - var payload common.CacheKeyPayload - if err := json.Unmarshal([]byte(value), &payload); err != nil { - l.Errorw("[SendSmsCode]: Unmarshal Error", logger.Field("error", err.Error()), logger.Field("value", value)) - } - if payload.Code != req.Code { - return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - } - err := l.svcCtx.UserModel.Delete(l.ctx, userInfo.Id) - if err != nil { - l.Errorw("update user password error", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update user password") - } - return nil -} diff --git a/internal/logic/app/user/getuseronlinetimestatisticslogic.go b/internal/logic/app/user/getuseronlinetimestatisticslogic.go deleted file mode 100644 index bc293b5..0000000 --- a/internal/logic/app/user/getuseronlinetimestatisticslogic.go +++ /dev/null @@ -1,115 +0,0 @@ -package user - -import ( - "context" - "sort" - "time" - - "github.com/perfect-panel/server/internal/model/user" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/constant" - "github.com/perfect-panel/server/pkg/logger" -) - -type GetUserOnlineTimeStatisticsLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Get user online time total -func NewGetUserOnlineTimeStatisticsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserOnlineTimeStatisticsLogic { - return &GetUserOnlineTimeStatisticsLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *GetUserOnlineTimeStatisticsLogic) GetUserOnlineTimeStatistics() (resp *types.GetUserOnlineTimeStatisticsResponse, err error) { - u := l.ctx.Value(constant.CtxKeyUser).(*user.User) - //获取历史最长在线时间 - var OnlineSeconds int64 - if err := l.svcCtx.DB.Model(user.DeviceOnlineRecord{}).Where("user_id = ?", u.Id).Select("online_seconds").Order("online_seconds desc").Limit(1).Scan(&OnlineSeconds).Error; err != nil { - l.Logger.Error(err) - } - - //获取历史连续最长在线天数 - var DurationDays int64 - if err := l.svcCtx.DB.Model(user.DeviceOnlineRecord{}).Where("user_id = ?", u.Id).Select("duration_days").Order("duration_days desc").Limit(1).Scan(&DurationDays).Error; err != nil { - l.Logger.Error(err) - } - - //获取近七天在线情况 - var userOnlineRecord []user.DeviceOnlineRecord - if err := l.svcCtx.DB.Model(&userOnlineRecord).Where("user_id = ? and created_at >= ?", u.Id, time.Now().AddDate(0, 0, -7).Format(time.DateTime)).Order("created_at desc").Find(&userOnlineRecord).Error; err != nil { - l.Logger.Error(err) - } - - //获取当前连续在线天数 - var currentContinuousDays int64 - if len(userOnlineRecord) > 0 { - currentContinuousDays = userOnlineRecord[0].DurationDays - } else { - currentContinuousDays = 1 - } - - var dates []string - for i := 0; i < 7; i++ { - date := time.Now().AddDate(0, 0, -i).Format(time.DateOnly) - dates = append(dates, date) - } - - onlineDays := make(map[string]types.WeeklyStat) - for _, record := range userOnlineRecord { - //获取近七天在线情况 - onlineTime := record.OnlineTime.Format(time.DateOnly) - if weeklyStat, ok := onlineDays[onlineTime]; ok { - weeklyStat.Hours += float64(record.OnlineSeconds) - onlineDays[onlineTime] = weeklyStat - } else { - onlineDays[onlineTime] = types.WeeklyStat{ - Hours: float64(record.OnlineSeconds), - //根据日期获取周几 - DayName: record.OnlineTime.Weekday().String(), - } - } - } - - //补全不存在的日期 - for _, date := range dates { - if _, ok := onlineDays[date]; !ok { - onlineTime, _ := time.Parse(time.DateOnly, date) - onlineDays[date] = types.WeeklyStat{ - DayName: onlineTime.Weekday().String(), - } - } - } - - var keys []string - for key := range onlineDays { - keys = append(keys, key) - } - - //排序 - sort.Strings(keys) - - var weeklyStats []types.WeeklyStat - for index, key := range keys { - weeklyStat := onlineDays[key] - weeklyStat.Day = index + 1 - weeklyStat.Hours = weeklyStat.Hours / float64(3600) - weeklyStats = append(weeklyStats, weeklyStat) - } - - resp = &types.GetUserOnlineTimeStatisticsResponse{ - WeeklyStats: weeklyStats, - ConnectionRecords: types.ConnectionRecords{ - CurrentContinuousDays: currentContinuousDays, - HistoryContinuousDays: DurationDays, - LongestSingleConnection: OnlineSeconds / 60, - }, - } - return -} diff --git a/internal/logic/app/user/getusersubscribetrafficlogslogic.go b/internal/logic/app/user/getusersubscribetrafficlogslogic.go deleted file mode 100644 index b7c5aca..0000000 --- a/internal/logic/app/user/getusersubscribetrafficlogslogic.go +++ /dev/null @@ -1,85 +0,0 @@ -package user - -import ( - "context" - "time" - - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" - - "github.com/perfect-panel/server/internal/model/traffic" - "github.com/perfect-panel/server/internal/model/user" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/constant" - "github.com/perfect-panel/server/pkg/logger" - "gorm.io/gorm" -) - -type GetUserSubscribeTrafficLogsLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Get user subcribe traffic logs -func NewGetUserSubscribeTrafficLogsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserSubscribeTrafficLogsLogic { - return &GetUserSubscribeTrafficLogsLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *GetUserSubscribeTrafficLogsLogic) GetUserSubscribeTrafficLogs(req *types.GetUserSubscribeTrafficLogsRequest) (resp *types.GetUserSubscribeTrafficLogsResponse, err error) { - resp = &types.GetUserSubscribeTrafficLogsResponse{} - u := l.ctx.Value(constant.CtxKeyUser).(*user.User) - var traffics []traffic.TrafficLog - err = l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { - return db.Model(traffic.TrafficLog{}).Where("user_id = ? and `timestamp` >= ? and `timestamp` < ?", u.Id, time.UnixMilli(req.StartTime), time.UnixMilli(req.EndTime)).Find(&traffics).Error - }) - - if err != nil { - l.Errorw("get user subscribe traffic logs failed", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), err.Error()) - } - - //合并多条记录为以天为单位 - trafficMap := make(map[string]*traffic.TrafficLog) - for _, traf := range traffics { - key := traf.Timestamp.Format(time.DateOnly) - existTraf := trafficMap[key] - if existTraf == nil { - trafficMap[key] = &traf - } else { - existTraf.Upload = existTraf.Download + traf.Upload - existTraf.Download = existTraf.Download + traf.Download - trafficMap[key] = existTraf - } - } - - startTime := time.UnixMilli(req.StartTime) - EndTime := time.UnixMilli(req.EndTime) - res := make(map[string]traffic.TrafficLog) - - // 循环遍历每一天 - for current := startTime; !current.After(EndTime); current = current.AddDate(0, 0, 1) { - dateStr := current.Format(time.DateOnly) // 格式化为日期字符串 - if trafficMap[dateStr] == nil { - res[dateStr] = traffic.TrafficLog{ - Timestamp: current, - } - } else { - res[dateStr] = *trafficMap[dateStr] - } - resp.List = append(resp.List, types.TrafficLog{ - Id: res[dateStr].Id, - ServerId: res[dateStr].ServerId, - Upload: res[dateStr].Upload, - Download: res[dateStr].Download, - Timestamp: res[dateStr].Timestamp.UnixMilli(), - }) - } - - return -} diff --git a/internal/logic/app/user/queryUserAffiliateListLogic.go b/internal/logic/app/user/queryUserAffiliateListLogic.go deleted file mode 100644 index 02319eb..0000000 --- a/internal/logic/app/user/queryUserAffiliateListLogic.go +++ /dev/null @@ -1,62 +0,0 @@ -package user - -import ( - "context" - - "github.com/perfect-panel/server/pkg/constant" - - "github.com/perfect-panel/server/internal/model/user" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" - "gorm.io/gorm" - - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" -) - -type QueryUserAffiliateListLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Query User Affiliate List -func NewQueryUserAffiliateListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryUserAffiliateListLogic { - return &QueryUserAffiliateListLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *QueryUserAffiliateListLogic) QueryUserAffiliateList(req *types.QueryUserAffiliateListRequest) (resp *types.QueryUserAffiliateListResponse, err error) { - u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) - if !ok { - logger.Error("current user is not found in context") - return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") - } - var data []*user.User - var total int64 - err = l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { - return db.Model(&user.User{}).Order("id desc").Where("referer_id = ?", u.Id).Count(&total).Limit(req.Size).Offset((req.Page - 1) * req.Size).Find(&data).Error - }) - if err != nil { - l.Errorw("Query User Affiliate List failed: %v", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query User Affiliate List failed: %v", err.Error()) - } - - list := make([]types.UserAffiliate, 0) - for _, item := range data { - list = append(list, types.UserAffiliate{ - //Email: tool.MaskEmail(item.Email), - Avatar: item.Avatar, - RegisteredAt: item.CreatedAt.UnixMilli(), - Enable: *item.Enable, - }) - } - return &types.QueryUserAffiliateListResponse{ - Total: total, - List: list, - }, nil -} diff --git a/internal/logic/app/user/queryUserInfoLogic.go b/internal/logic/app/user/queryUserInfoLogic.go deleted file mode 100644 index 9bc70e7..0000000 --- a/internal/logic/app/user/queryUserInfoLogic.go +++ /dev/null @@ -1,63 +0,0 @@ -package user - -import ( - "context" - - "github.com/perfect-panel/server/pkg/constant" - - "github.com/perfect-panel/server/internal/model/user" - - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" -) - -type QueryUserInfoLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// query user info -func NewQueryUserInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryUserInfoLogic { - return &QueryUserInfoLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *QueryUserInfoLogic) QueryUserInfo() (resp *types.UserInfoResponse, err error) { - u := l.ctx.Value(constant.CtxKeyUser).(*user.User) - var devices []types.UserDevice - if len(u.UserDevices) != 0 { - for _, device := range u.UserDevices { - devices = append(devices, types.UserDevice{ - Id: device.Id, - Identifier: device.Identifier, - Online: device.Online, - }) - } - } - var authMeths []types.UserAuthMethod - authMethods, err := l.svcCtx.UserModel.FindUserAuthMethods(l.ctx, u.Id) - if err == nil && len(authMeths) != 0 { - for _, as := range authMethods { - authMeths = append(authMeths, types.UserAuthMethod{ - AuthType: as.AuthType, - AuthIdentifier: as.AuthIdentifier, - }) - } - } - - resp = &types.UserInfoResponse{ - Id: u.Id, - Balance: u.Balance, - Avatar: u.Avatar, - ReferCode: u.ReferCode, - RefererId: u.RefererId, - Devices: devices, - AuthMethods: authMeths, - } - return -} diff --git a/internal/logic/app/user/queryuseraffiliatelogic.go b/internal/logic/app/user/queryuseraffiliatelogic.go deleted file mode 100644 index 48d7bc6..0000000 --- a/internal/logic/app/user/queryuseraffiliatelogic.go +++ /dev/null @@ -1,60 +0,0 @@ -package user - -import ( - "context" - - "github.com/perfect-panel/server/internal/model/user" - "github.com/perfect-panel/server/pkg/constant" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" - "gorm.io/gorm" - - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" -) - -type QueryUserAffiliateLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Query User Affiliate Count -func NewQueryUserAffiliateLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryUserAffiliateLogic { - return &QueryUserAffiliateLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *QueryUserAffiliateLogic) QueryUserAffiliate() (resp *types.QueryUserAffiliateCountResponse, err error) { - u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) - if !ok { - logger.Error("current user is not found in context") - return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") - } - var sum int64 - var total int64 - err = l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { - return db.Model(&user.User{}).Where("referer_id = ?", u.Id).Count(&total).Find(&user.User{}).Error - }) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query User Affiliate failed: %v", err) - } - err = l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { - return db.Model(&user.CommissionLog{}). - Where("user_id = ?", u.Id). - Select("COALESCE(SUM(amount), 0)"). - Scan(&sum).Error - }) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query User Affiliate failed: %v", err) - } - - return &types.QueryUserAffiliateCountResponse{ - Registers: total, - TotalCommission: sum, - }, nil -} diff --git a/internal/logic/app/user/updatePasswordLogic.go b/internal/logic/app/user/updatePasswordLogic.go deleted file mode 100644 index 4dee24d..0000000 --- a/internal/logic/app/user/updatePasswordLogic.go +++ /dev/null @@ -1,46 +0,0 @@ -package user - -import ( - "context" - - "github.com/perfect-panel/server/pkg/constant" - - "github.com/perfect-panel/server/internal/model/user" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/tool" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" -) - -type UpdatePasswordLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Update Password -func NewUpdatePasswordLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdatePasswordLogic { - return &UpdatePasswordLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *UpdatePasswordLogic) UpdatePassword(req *types.UpdatePasswordRequeset) error { - userInfo := l.ctx.Value(constant.CtxKeyUser).(*user.User) - - // Verify password - if !tool.VerifyPassWord(req.Password, userInfo.Password) { - return errors.Wrapf(xerr.NewErrCode(xerr.UserPasswordError), "user password") - } - userInfo.Password = tool.EncodePassWord(req.NewPassword) - err := l.svcCtx.UserModel.Update(l.ctx, userInfo) - if err != nil { - l.Errorw("update user password error", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update user password") - } - return err -} diff --git a/internal/logic/app/ws/appWsLogic.go b/internal/logic/app/ws/appWsLogic.go deleted file mode 100644 index d86de91..0000000 --- a/internal/logic/app/ws/appWsLogic.go +++ /dev/null @@ -1,81 +0,0 @@ -package ws - -import ( - "context" - "net/http" - "strconv" - "time" - - "github.com/perfect-panel/server/internal/model/user" - "github.com/perfect-panel/server/pkg/constant" - "github.com/perfect-panel/server/pkg/xerr" - - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/pkg/logger" -) - -type AppWsLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// App heartbeat -func NewAppWsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AppWsLogic { - return &AppWsLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *AppWsLogic) AppWs(w http.ResponseWriter, r *http.Request, userid, identifier string) error { - //获取设备号 - if identifier == "" { - return xerr.NewErrCode(xerr.DeviceNotExist) - } - //获取用户id - userID, err := strconv.ParseInt(userid, 10, 64) - if err != nil { - return xerr.NewErrCode(xerr.UseridNotMatch) - } - - ////获取session - value := l.ctx.Value(constant.CtxKeySessionID) - if value == nil { - return xerr.NewErrCode(xerr.ErrorTokenInvalid) - } - session := value.(string) - - //获取用户 - userInfo := l.ctx.Value(constant.CtxKeyUser).(*user.User) - - if userID != userInfo.Id { - return xerr.NewErrCode(xerr.UseridNotMatch) - } - - _, err = l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, identifier) - if err != nil { - return xerr.NewErrCode(xerr.DeviceNotExist) - } - - //if device.UserId != userInfo.Id { - // return xerr.NewErrCode(xerr.DeviceNotExist) - //} - - //默认在线设备1 - maxDevice := 0 - subscribe, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, userInfo.Id) - if err == nil { - for _, sub := range subscribe { - if time.Now().Before(sub.ExpireTime) { - deviceLimit := int(sub.Subscribe.DeviceLimit) - if deviceLimit > maxDevice { - maxDevice = deviceLimit - } - } - } - } - l.svcCtx.DeviceManager.AddDevice(w, r, session, userID, identifier, maxDevice) - return nil -} diff --git a/internal/logic/auth/bindDeviceLogic.go b/internal/logic/auth/bindDeviceLogic.go new file mode 100644 index 0000000..19b0666 --- /dev/null +++ b/internal/logic/auth/bindDeviceLogic.go @@ -0,0 +1,234 @@ +package auth + +import ( + "context" + + "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/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type BindDeviceLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewBindDeviceLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BindDeviceLogic { + return &BindDeviceLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// BindDeviceToUser binds a device to a user +// If the device is already bound to another user, it will disable that user and bind the device to the current user +func (l *BindDeviceLogic) BindDeviceToUser(identifier, ip, userAgent string, currentUserId int64) error { + if identifier == "" { + // No device identifier provided, skip binding + return nil + } + + l.Infow("binding device to user", + logger.Field("identifier", identifier), + logger.Field("user_id", currentUserId), + logger.Field("ip", ip), + ) + + // Check if device exists + deviceInfo, err := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, identifier) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + // Device not found, create new device record + return l.createDeviceForUser(identifier, ip, userAgent, currentUserId) + } + l.Errorw("failed to query device", + logger.Field("identifier", identifier), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query device failed: %v", err.Error()) + } + + // Device exists, check if it's bound to current user + if deviceInfo.UserId == currentUserId { + // Already bound to current user, just update IP and UserAgent + l.Infow("device already bound to current user, updating info", + logger.Field("identifier", identifier), + logger.Field("user_id", currentUserId), + ) + deviceInfo.Ip = ip + deviceInfo.UserAgent = userAgent + if err := l.svcCtx.UserModel.UpdateDevice(l.ctx, deviceInfo); err != nil { + l.Errorw("failed to update device", + logger.Field("identifier", identifier), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update device failed: %v", err.Error()) + } + return nil + } + + // Device is bound to another user, need to disable old user and rebind + l.Infow("device bound to another user, rebinding", + logger.Field("identifier", identifier), + logger.Field("old_user_id", deviceInfo.UserId), + logger.Field("new_user_id", currentUserId), + ) + + return l.rebindDeviceToNewUser(deviceInfo, ip, userAgent, currentUserId) +} + +func (l *BindDeviceLogic) createDeviceForUser(identifier, ip, userAgent string, userId int64) error { + l.Infow("creating new device for user", + logger.Field("identifier", identifier), + logger.Field("user_id", userId), + ) + + err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { + // Create device auth method + authMethod := &user.AuthMethods{ + UserId: userId, + AuthType: "device", + AuthIdentifier: identifier, + Verified: true, + } + if err := db.Create(authMethod).Error; err != nil { + l.Errorw("failed to create device auth method", + logger.Field("user_id", userId), + logger.Field("identifier", identifier), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create device auth method failed: %v", err) + } + + // Create device record + deviceInfo := &user.Device{ + Ip: ip, + UserId: userId, + UserAgent: userAgent, + Identifier: identifier, + Enabled: true, + Online: false, + } + if err := db.Create(deviceInfo).Error; err != nil { + l.Errorw("failed to create device", + logger.Field("user_id", userId), + logger.Field("identifier", identifier), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create device failed: %v", err) + } + + return nil + }) + + if err != nil { + l.Errorw("device creation failed", + logger.Field("identifier", identifier), + logger.Field("user_id", userId), + logger.Field("error", err.Error()), + ) + return err + } + + l.Infow("device created successfully", + logger.Field("identifier", identifier), + logger.Field("user_id", userId), + ) + + return nil +} + +func (l *BindDeviceLogic) rebindDeviceToNewUser(deviceInfo *user.Device, ip, userAgent string, newUserId int64) error { + oldUserId := deviceInfo.UserId + + err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { + // Check if old user has other auth methods besides device + var authMethods []user.AuthMethods + if err := db.Where("user_id = ?", oldUserId).Find(&authMethods).Error; err != nil { + l.Errorw("failed to query auth methods for old user", + logger.Field("old_user_id", oldUserId), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query auth methods failed: %v", err) + } + + // Count non-device auth methods + nonDeviceAuthCount := 0 + for _, auth := range authMethods { + if auth.AuthType != "device" { + nonDeviceAuthCount++ + } + } + + // Only disable old user if they have no other auth methods + if nonDeviceAuthCount == 0 { + falseVal := false + if err := db.Model(&user.User{}).Where("id = ?", oldUserId).Update("enable", &falseVal).Error; err != nil { + l.Errorw("failed to disable old user", + logger.Field("old_user_id", oldUserId), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "disable old user failed: %v", err) + } + + l.Infow("disabled old user (no other auth methods)", + logger.Field("old_user_id", oldUserId), + ) + } else { + l.Infow("old user has other auth methods, not disabling", + logger.Field("old_user_id", oldUserId), + logger.Field("non_device_auth_count", nonDeviceAuthCount), + ) + } + + // Update device auth method to new user + if err := db.Model(&user.AuthMethods{}). + Where("auth_type = ? AND auth_identifier = ?", "device", deviceInfo.Identifier). + Update("user_id", newUserId).Error; err != nil { + l.Errorw("failed to update device auth method", + logger.Field("identifier", deviceInfo.Identifier), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update device auth method failed: %v", err) + } + + // Update device record + deviceInfo.UserId = newUserId + deviceInfo.Ip = ip + deviceInfo.UserAgent = userAgent + deviceInfo.Enabled = true + + if err := db.Save(deviceInfo).Error; err != nil { + l.Errorw("failed to update device", + logger.Field("identifier", deviceInfo.Identifier), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update device failed: %v", err) + } + + return nil + }) + + if err != nil { + l.Errorw("device rebinding failed", + logger.Field("identifier", deviceInfo.Identifier), + logger.Field("old_user_id", oldUserId), + logger.Field("new_user_id", newUserId), + logger.Field("error", err.Error()), + ) + return err + } + + l.Infow("device rebound successfully", + logger.Field("identifier", deviceInfo.Identifier), + logger.Field("old_user_id", oldUserId), + logger.Field("new_user_id", newUserId), + ) + + return nil +} diff --git a/internal/logic/auth/deviceLoginLogic.go b/internal/logic/auth/deviceLoginLogic.go new file mode 100644 index 0000000..2e2b6ae --- /dev/null +++ b/internal/logic/auth/deviceLoginLogic.go @@ -0,0 +1,293 @@ +package auth + +import ( + "context" + "fmt" + "time" + + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/jwt" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/uuidx" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type DeviceLoginLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Device Login +func NewDeviceLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeviceLoginLogic { + return &DeviceLoginLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *DeviceLoginLogic) DeviceLogin(req *types.DeviceLoginRequest) (resp *types.LoginResponse, err error) { + if !l.svcCtx.Config.Device.Enable { + return nil, xerr.NewErrMsg("Device login is disabled") + } + + loginStatus := false + var userInfo *user.User + // Record login status + defer func() { + if userInfo != nil && userInfo.Id != 0 { + loginLog := log.Login{ + Method: "device", + LoginIP: req.IP, + UserAgent: req.UserAgent, + Success: loginStatus, + Timestamp: time.Now().UnixMilli(), + } + content, _ := loginLog.Marshal() + if err := l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + Type: log.TypeLogin.Uint8(), + Date: time.Now().Format("2006-01-02"), + ObjectID: userInfo.Id, + Content: string(content), + }); err != nil { + l.Errorw("failed to insert login log", + logger.Field("user_id", userInfo.Id), + logger.Field("ip", req.IP), + logger.Field("error", err.Error()), + ) + } + } + }() + + // Check if device exists by identifier + deviceInfo, err := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + // Device not found, create new user and device + userInfo, err = l.registerUserAndDevice(req) + if err != nil { + return nil, err + } + } else { + l.Errorw("query device failed", + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query device failed: %v", err.Error()) + } + } else { + // Device found, get user info + userInfo, err = l.svcCtx.UserModel.FindOne(l.ctx, deviceInfo.UserId) + if err != nil { + l.Errorw("query user failed", + logger.Field("user_id", deviceInfo.UserId), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user failed: %v", err.Error()) + } + } + + // Generate session id + sessionId := uuidx.NewUUID().String() + + // Generate token + token, err := jwt.NewJwtToken( + l.svcCtx.Config.JwtAuth.AccessSecret, + time.Now().Unix(), + l.svcCtx.Config.JwtAuth.AccessExpire, + jwt.WithOption("UserId", userInfo.Id), + jwt.WithOption("SessionId", sessionId), + jwt.WithOption("LoginType", "device"), + ) + if err != nil { + l.Errorw("token generate error", + logger.Field("user_id", userInfo.Id), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "token generate error: %v", err.Error()) + } + + // Store session id in redis + sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId) + if err = l.svcCtx.Redis.Set(l.ctx, sessionIdCacheKey, userInfo.Id, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil { + l.Errorw("set session id error", + logger.Field("user_id", userInfo.Id), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err.Error()) + } + + loginStatus = true + return &types.LoginResponse{ + Token: token, + }, nil +} + +func (l *DeviceLoginLogic) registerUserAndDevice(req *types.DeviceLoginRequest) (*user.User, error) { + l.Infow("device not found, creating new user and device", + logger.Field("identifier", req.Identifier), + logger.Field("ip", req.IP), + ) + + var userInfo *user.User + err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { + // Create new user + userInfo = &user.User{ + OnlyFirstPurchase: &l.svcCtx.Config.Invite.OnlyFirstPurchase, + } + if err := db.Create(userInfo).Error; err != nil { + l.Errorw("failed to create user", + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create user failed: %v", err) + } + + // Update refer code + userInfo.ReferCode = uuidx.UserInviteCode(userInfo.Id) + if err := db.Model(&user.User{}).Where("id = ?", userInfo.Id).Update("refer_code", userInfo.ReferCode).Error; err != nil { + l.Errorw("failed to update refer code", + logger.Field("user_id", userInfo.Id), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update refer code failed: %v", err) + } + + // Create device auth method + authMethod := &user.AuthMethods{ + UserId: userInfo.Id, + AuthType: "device", + AuthIdentifier: req.Identifier, + Verified: true, + } + if err := db.Create(authMethod).Error; err != nil { + l.Errorw("failed to create device auth method", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create device auth method failed: %v", err) + } + + // Insert device record + deviceInfo := &user.Device{ + Ip: req.IP, + UserId: userInfo.Id, + UserAgent: req.UserAgent, + Identifier: req.Identifier, + Enabled: true, + Online: false, + } + if err := db.Create(deviceInfo).Error; err != nil { + l.Errorw("failed to insert device", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert device failed: %v", err) + } + + // Activate trial if enabled + if l.svcCtx.Config.Register.EnableTrial { + if err := l.activeTrial(userInfo.Id, db); err != nil { + return err + } + } + + return nil + }) + + if err != nil { + l.Errorw("device registration failed", + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + return nil, err + } + + l.Infow("device registration completed successfully", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", req.Identifier), + logger.Field("refer_code", userInfo.ReferCode), + ) + + // Register log + registerLog := log.Register{ + AuthMethod: "device", + Identifier: req.Identifier, + RegisterIP: req.IP, + UserAgent: req.UserAgent, + Timestamp: time.Now().UnixMilli(), + } + content, _ := registerLog.Marshal() + + if err := l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + Type: log.TypeRegister.Uint8(), + Date: time.Now().Format("2006-01-02"), + ObjectID: userInfo.Id, + Content: string(content), + }); err != nil { + l.Errorw("failed to insert register log", + logger.Field("user_id", userInfo.Id), + logger.Field("ip", req.IP), + logger.Field("error", err.Error()), + ) + } + + return userInfo, nil +} + +func (l *DeviceLoginLogic) activeTrial(userId int64, db *gorm.DB) error { + sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, l.svcCtx.Config.Register.TrialSubscribe) + if err != nil { + l.Errorw("failed to find trial subscription template", + logger.Field("user_id", userId), + logger.Field("trial_subscribe_id", l.svcCtx.Config.Register.TrialSubscribe), + logger.Field("error", err.Error()), + ) + return err + } + + startTime := time.Now() + expireTime := tool.AddTime(l.svcCtx.Config.Register.TrialTimeUnit, l.svcCtx.Config.Register.TrialTime, startTime) + subscribeToken := uuidx.SubscribeToken(fmt.Sprintf("Trial-%v", userId)) + subscribeUUID := uuidx.NewUUID().String() + + userSub := &user.Subscribe{ + UserId: userId, + OrderId: 0, + SubscribeId: sub.Id, + StartTime: startTime, + ExpireTime: expireTime, + Traffic: sub.Traffic, + Download: 0, + Upload: 0, + Token: subscribeToken, + UUID: subscribeUUID, + Status: 1, + } + + if err := db.Create(userSub).Error; err != nil { + l.Errorw("failed to insert trial subscription", + logger.Field("user_id", userId), + logger.Field("error", err.Error()), + ) + return err + } + + l.Infow("trial subscription activated successfully", + logger.Field("user_id", userId), + logger.Field("subscribe_id", sub.Id), + logger.Field("expire_time", expireTime), + logger.Field("traffic", sub.Traffic), + ) + + return nil +} diff --git a/internal/logic/auth/oauth/oAuthLoginGetTokenLogic.go b/internal/logic/auth/oauth/oAuthLoginGetTokenLogic.go index b2dc83c..4e12d2f 100644 --- a/internal/logic/auth/oauth/oAuthLoginGetTokenLogic.go +++ b/internal/logic/auth/oauth/oAuthLoginGetTokenLogic.go @@ -8,6 +8,7 @@ import ( "github.com/perfect-panel/server/internal/config" "github.com/perfect-panel/server/internal/model/auth" + "github.com/perfect-panel/server/internal/model/log" "github.com/perfect-panel/server/internal/model/user" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" @@ -23,7 +24,16 @@ import ( "gorm.io/gorm" ) -type googleRequest struct { +const ( + OAuthGoogle = "google" + OAuthApple = "apple" + OAuthTelegram = "telegram" + AuthEmail = "email" + AuthExpire = 86400 + TelegramDomain = "ppanel.com" +) + +type oauthRequest struct { Code string `json:"code"` State string `json:"state"` } @@ -43,38 +53,524 @@ func NewOAuthLoginGetTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) } func (l *OAuthLoginGetTokenLogic) OAuthLoginGetToken(req *types.OAuthLoginGetTokenRequest, ip, userAgent string) (resp *types.LoginResponse, err error) { + requestID := uuidx.NewUUID().String() loginStatus := false var userInfo *user.User - // Record login status - defer func(svcCtx *svc.ServiceContext) { - if userInfo != nil && userInfo.Id != 0 { - if err := svcCtx.UserModel.InsertLoginLog(l.ctx, &user.LoginLog{ - UserId: userInfo.Id, - LoginIP: ip, - UserAgent: userAgent, - Success: &loginStatus, - }); err != nil { - l.Errorw("error insert login log: %v", logger.Field("error", err.Error())) - } - } - }(l.svcCtx) - switch req.Method { - case "google": - userInfo, err = l.google(req) - case "apple": - userInfo, err = l.apple(req) - case "telegram": - userInfo, err = l.telegram(req) - default: - l.Errorw("oauth login method not support: %v", logger.Field("method", req.Method)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "oauth login method not support: %v", req.Method) - } + + l.Infow("oauth login request started", + logger.Field("request_id", requestID), + logger.Field("method", req.Method), + logger.Field("ip", ip), + logger.Field("user_agent", userAgent), + ) + + defer func() { + l.recordLoginStatus(loginStatus, userInfo, ip, userAgent, requestID, req.Method) + }() + + userInfo, err = l.handleOAuthProvider(req, requestID, ip, userAgent) if err != nil { return nil, err } - // Generate session id + + token, err := l.generateToken(userInfo, requestID) + if err != nil { + return nil, err + } + + loginStatus = true + return &types.LoginResponse{Token: token}, nil +} + +func (l *OAuthLoginGetTokenLogic) google(req *types.OAuthLoginGetTokenRequest, requestID, ip, userAgent string) (*user.User, error) { + startTime := time.Now() + l.Infow("google oauth processing started", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthGoogle), + ) + + var request oauthRequest + if err := tool.CloneMapToStruct(req.Callback.(map[string]interface{}), &request); err != nil { + l.Errorw("failed to parse google callback data", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthGoogle), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "parse callback data failed: %v", err) + } + + l.Debugw("google oauth state validation started", + logger.Field("request_id", requestID), + logger.Field("state", request.State), + ) + + redirect, err := l.validateStateCode(OAuthGoogle, request.State, requestID) + if err != nil { + return nil, err + } + + cfg, err := l.getGoogleConfig(requestID) + if err != nil { + return nil, err + } + + client := google.New(&google.Config{ + ClientID: cfg.ClientId, + ClientSecret: cfg.ClientSecret, + RedirectURL: redirect, + }) + + l.Debugw("exchanging google authorization code for token", + logger.Field("request_id", requestID), + logger.Field("redirect_url", redirect), + ) + + token, err := client.Exchange(l.ctx, request.Code) + if err != nil { + l.Errorw("failed to exchange google authorization code", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthGoogle), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "exchange token failed: %v", err) + } + + l.Debugw("fetching google user information", + logger.Field("request_id", requestID), + ) + + googleUserInfo, err := client.GetUserInfo(token.AccessToken) + if err != nil { + l.Errorw("failed to get google user info", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthGoogle), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "get user info failed: %v", err) + } + + l.Infow("google oauth processing completed", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthGoogle), + logger.Field("openid", googleUserInfo.OpenID), + logger.Field("email", googleUserInfo.Email), + logger.Field("duration_ms", time.Since(startTime).Milliseconds()), + ) + + return l.findOrRegisterUser(OAuthGoogle, googleUserInfo.OpenID, googleUserInfo.Email, googleUserInfo.Picture, requestID, ip, userAgent) +} + +func (l *OAuthLoginGetTokenLogic) apple(req *types.OAuthLoginGetTokenRequest, requestID, ip, userAgent string) (*user.User, error) { + startTime := time.Now() + l.Infow("apple oauth processing started", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthApple), + ) + + callback := req.Callback.(map[string]interface{}) + state, _ := callback["state"].(string) + code, _ := callback["code"].(string) + + l.Debugw("apple oauth state validation started", + logger.Field("request_id", requestID), + logger.Field("state", state), + ) + + if _, err := l.validateStateCode(OAuthApple, state, requestID); err != nil { + return nil, err + } + + cfg, err := l.getAppleConfig(requestID) + if err != nil { + return nil, err + } + + client, err := apple.New(apple.Config{ + ClientID: cfg.ClientId, + TeamID: cfg.TeamID, + KeyID: cfg.KeyID, + ClientSecret: cfg.ClientSecret, + RedirectURI: cfg.RedirectURL, + }) + if err != nil { + l.Errorw("failed to create apple client", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthApple), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "new apple client failed: %v", err) + } + + l.Debugw("verifying apple web token", + logger.Field("request_id", requestID), + ) + + resp, err := client.VerifyWebToken(l.ctx, code) + if err != nil { + l.Errorw("failed to verify apple web token", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthApple), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "verify web token failed: %v", err) + } + + if resp.Error != "" { + l.Errorw("apple web token verification returned error", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthApple), + logger.Field("apple_error", resp.Error), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "verify web token failed: %v", resp.Error) + } + + appleUnique, err := apple.GetUniqueID(resp.IDToken) + if err != nil { + l.Errorw("failed to get apple unique id", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthApple), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "get apple unique id failed: %v", err) + } + + appleUserInfo, err := apple.GetClaims(resp.AccessToken) + if err != nil { + l.Errorw("failed to get apple user claims", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthApple), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "get apple user info failed: %v", err) + } + + email := "" + if emailVal, ok := (*appleUserInfo)["email"]; ok { + email, _ = emailVal.(string) + } + + l.Infow("apple oauth processing completed", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthApple), + logger.Field("unique_id", appleUnique), + logger.Field("email", email), + logger.Field("duration_ms", time.Since(startTime).Milliseconds()), + ) + + return l.findOrRegisterUser(OAuthApple, appleUnique, email, "", requestID, ip, userAgent) +} + +func (l *OAuthLoginGetTokenLogic) telegram(req *types.OAuthLoginGetTokenRequest, requestID, ip, userAgent string) (*user.User, error) { + startTime := time.Now() + l.Infow("telegram oauth processing started", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthTelegram), + ) + + cfg, err := l.getTelegramConfig(requestID) + if err != nil { + return nil, err + } + + encodeText, _ := req.Callback.(map[string]interface{})["tgAuthResult"].(string) + l.Debugw("parsing telegram callback data", + logger.Field("request_id", requestID), + logger.Field("data_length", len(encodeText)), + ) + + callbackData, err := telegram.ParseAndValidateBase64([]byte(encodeText), cfg.BotToken) + if err != nil { + l.Errorw("failed to parse telegram callback data", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthTelegram), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "parse telegram callback failed: %v", err) + } + + l.Debugw("validating telegram auth date", + logger.Field("request_id", requestID), + logger.Field("auth_date", *callbackData.AuthDate), + logger.Field("current_time", time.Now().Unix()), + ) + + if time.Now().Unix()-*callbackData.AuthDate > AuthExpire { + l.Errorw("telegram auth date expired", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthTelegram), + logger.Field("auth_date", *callbackData.AuthDate), + logger.Field("current_time", time.Now().Unix()), + logger.Field("expire_seconds", AuthExpire), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "auth date expired") + } + + userID := fmt.Sprintf("%v", *callbackData.Id) + email := fmt.Sprintf("%v@%s", *callbackData.Id, TelegramDomain) + avatar := "" + if callbackData.PhotoUrl != nil { + avatar = *callbackData.PhotoUrl + } + + l.Infow("telegram oauth processing completed", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthTelegram), + logger.Field("user_id", userID), + logger.Field("email", email), + logger.Field("duration_ms", time.Since(startTime).Milliseconds()), + ) + + return l.findOrRegisterUser(OAuthTelegram, userID, email, avatar, requestID, ip, userAgent) +} + +func (l *OAuthLoginGetTokenLogic) register(email, avatar, method, openid, requestID, ip, userAgent string) (*user.User, error) { + startTime := time.Now() + l.Infow("user registration started", + logger.Field("request_id", requestID), + logger.Field("auth_method", method), + logger.Field("email", email), + logger.Field("openid", openid), + ) + + if l.svcCtx.Config.Invite.ForcedInvite { + l.Errorw("registration blocked due to forced invite policy", + logger.Field("request_id", requestID), + logger.Field("auth_method", method), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InviteCodeError), "invite code is required") + } + + var userInfo *user.User + err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { + if email != "" { + l.Debugw("checking if email already exists", + logger.Field("request_id", requestID), + logger.Field("email", email), + ) + if err := l.checkEmailExists(db, email, requestID); err != nil { + return err + } + } + + l.Debugw("creating new user record", + logger.Field("request_id", requestID), + logger.Field("avatar", avatar), + ) + + userInfo = &user.User{Avatar: avatar, OnlyFirstPurchase: &l.svcCtx.Config.Invite.OnlyFirstPurchase} + if err := db.Create(userInfo).Error; err != nil { + l.Errorw("failed to create user record", + logger.Field("request_id", requestID), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create user info failed: %v", err) + } + + userInfo.ReferCode = uuidx.UserInviteCode(userInfo.Id) + l.Debugw("updating user refer code", + logger.Field("request_id", requestID), + logger.Field("user_id", userInfo.Id), + logger.Field("refer_code", userInfo.ReferCode), + ) + + if err := db.Model(&user.User{}).Where("id = ?", userInfo.Id).Update("refer_code", userInfo.ReferCode).Error; err != nil { + l.Errorw("failed to update refer code", + logger.Field("request_id", requestID), + logger.Field("user_id", userInfo.Id), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update refer code failed: %v", err) + } + + if err := l.createAuthMethod(db, userInfo.Id, method, openid, requestID); err != nil { + return err + } + + if email != "" { + if err := l.createAuthMethod(db, userInfo.Id, AuthEmail, email, requestID); err != nil { + return err + } + } + + if l.svcCtx.Config.Register.EnableTrial { + l.Debugw("activating trial subscription", + logger.Field("request_id", requestID), + logger.Field("user_id", userInfo.Id), + ) + if err := l.activeTrial(userInfo.Id, requestID); err != nil { + return err + } + } + + return nil + }) + + if err != nil { + l.Errorw("user registration failed", + logger.Field("request_id", requestID), + logger.Field("auth_method", method), + logger.Field("error", err.Error()), + logger.Field("duration_ms", time.Since(startTime).Milliseconds()), + ) + return userInfo, err + } + + l.Infow("user registration completed successfully", + logger.Field("request_id", requestID), + logger.Field("user_id", userInfo.Id), + logger.Field("auth_method", method), + logger.Field("email", email), + logger.Field("refer_code", userInfo.ReferCode), + logger.Field("duration_ms", time.Since(startTime).Milliseconds()), + ) + + // Register log + registerLog := log.Register{ + AuthMethod: method, + Identifier: openid, + RegisterIP: ip, + UserAgent: userAgent, + Timestamp: time.Now().UnixMilli(), + } + content, _ := registerLog.Marshal() + + err = l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + Type: log.TypeRegister.Uint8(), + Date: time.Now().Format("2006-01-02"), + ObjectID: userInfo.Id, + Content: string(content), + }) + if err != nil { + l.Errorw("failed to insert register log", + logger.Field("request_id", requestID), + logger.Field("user_id", userInfo.Id), + logger.Field("ip", ip), + logger.Field("error", err.Error()), + ) + } + + return userInfo, err +} + +func (l *OAuthLoginGetTokenLogic) checkEmailExists(db *gorm.DB, email, requestID string) error { + var methodInfo user.AuthMethods + err := db.Model(&user.AuthMethods{}).Where("auth_identifier = ?", email).First(&methodInfo).Error + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + l.Errorw("failed to check email existence", + logger.Field("request_id", requestID), + logger.Field("email", email), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "check email exists failed: %v", err) + } + if methodInfo.UserId != 0 { + l.Errorw("email already exists for another user", + logger.Field("request_id", requestID), + logger.Field("email", email), + logger.Field("existing_user_id", methodInfo.UserId), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.UserExist), "user email exist: %v", email) + } + l.Debugw("email availability confirmed", + logger.Field("request_id", requestID), + logger.Field("email", email), + ) + return nil +} + +func (l *OAuthLoginGetTokenLogic) createAuthMethod(db *gorm.DB, userID int64, authType, identifier, requestID string) error { + l.Debugw("creating auth method", + logger.Field("request_id", requestID), + logger.Field("user_id", userID), + logger.Field("auth_type", authType), + logger.Field("identifier", identifier), + ) + + authMethod := &user.AuthMethods{ + UserId: userID, + AuthType: authType, + AuthIdentifier: identifier, + Verified: true, + } + if err := db.Create(authMethod).Error; err != nil { + l.Errorw("failed to create auth method", + logger.Field("request_id", requestID), + logger.Field("user_id", userID), + logger.Field("auth_type", authType), + logger.Field("identifier", identifier), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create auth method failed: %v", err) + } + + l.Debugw("auth method created successfully", + logger.Field("request_id", requestID), + logger.Field("user_id", userID), + logger.Field("auth_type", authType), + logger.Field("auth_method_id", authMethod.Id), + ) + return nil +} + +func (l *OAuthLoginGetTokenLogic) recordLoginStatus(loginStatus bool, userInfo *user.User, ip, userAgent, requestID, authType string) { + + if userInfo != nil && userInfo.Id != 0 { + loginLog := log.Login{ + Method: authType, + LoginIP: ip, + UserAgent: userAgent, + Success: loginStatus, + Timestamp: time.Now().UnixMilli(), + } + content, _ := loginLog.Marshal() + if err := l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + Type: log.TypeLogin.Uint8(), + Date: time.Now().Format("2006-01-02"), + ObjectID: userInfo.Id, + Content: string(content), + }); err != nil { + l.Errorw("failed to insert login log", + logger.Field("request_id", requestID), + logger.Field("user_id", userInfo.Id), + logger.Field("ip", ip), + logger.Field("error", err.Error()), + ) + } + } +} + +func (l *OAuthLoginGetTokenLogic) handleOAuthProvider(req *types.OAuthLoginGetTokenRequest, requestID, ip, userAgent string) (*user.User, error) { + l.Debugw("handling oauth provider", + logger.Field("request_id", requestID), + logger.Field("provider", req.Method), + ) + + switch req.Method { + case OAuthGoogle: + return l.google(req, requestID, ip, userAgent) + case OAuthApple: + return l.apple(req, requestID, ip, userAgent) + case OAuthTelegram: + return l.telegram(req, requestID, ip, userAgent) + default: + l.Errorw("unsupported oauth login method", + logger.Field("request_id", requestID), + logger.Field("method", req.Method), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "oauth login method not supported: %v", req.Method) + } +} + +func (l *OAuthLoginGetTokenLogic) generateToken(userInfo *user.User, requestID string) (string, error) { + startTime := time.Now() sessionId := uuidx.NewUUID().String() - // Generate token + + l.Debugw("generating jwt token", + logger.Field("request_id", requestID), + logger.Field("user_id", userInfo.Id), + logger.Field("session_id", sessionId), + ) + token, err := jwt.NewJwtToken( l.svcCtx.Config.JwtAuth.AccessSecret, time.Now().Unix(), @@ -83,262 +579,284 @@ func (l *OAuthLoginGetTokenLogic) OAuthLoginGetToken(req *types.OAuthLoginGetTok jwt.WithOption("SessionId", sessionId), ) if err != nil { - l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "token generate error: %v", err.Error()) + l.Errorw("failed to generate jwt token", + logger.Field("request_id", requestID), + logger.Field("user_id", userInfo.Id), + logger.Field("error", err.Error()), + ) + return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "token generate error: %v", err) } sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId) if err = l.svcCtx.Redis.Set(l.ctx, sessionIdCacheKey, userInfo.Id, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err.Error()) - } - loginStatus = true - return &types.LoginResponse{ - Token: token, - }, nil -} - -func (l *OAuthLoginGetTokenLogic) google(req *types.OAuthLoginGetTokenRequest) (*user.User, error) { - var request googleRequest - err := tool.CloneMapToStruct(req.Callback.(map[string]interface{}), &request) - if err != nil { - l.Errorw("error CloneMapToStruct: %v", logger.Field("error", err.Error())) - return nil, err - } - // validate the state code - redirect, err := l.svcCtx.Redis.Get(l.ctx, fmt.Sprintf("google:%s", request.State)).Result() - if err != nil { - l.Errorw("error get google state code: %v", logger.Field("error", err.Error())) - return nil, err - } - // get google config - authMethod, err := l.svcCtx.AuthModel.FindOneByMethod(l.ctx, "google") - if err != nil { - l.Errorw("error find google auth method: %v", logger.Field("error", err.Error())) - return nil, err - } - var cfg auth.GoogleAuthConfig - err = cfg.Unmarshal(authMethod.Config) - if err != nil { - l.Errorw("error unmarshal google config: %v", logger.Field("config", authMethod.Config), logger.Field("error", err.Error())) - return nil, err - } - client := google.New(&google.Config{ - ClientID: cfg.ClientId, - ClientSecret: cfg.ClientSecret, - RedirectURL: redirect, - }) - token, err := client.Exchange(l.ctx, request.Code) - if err != nil { - l.Errorw("error exchange google token: %v", logger.Field("error", err.Error())) - return nil, err - } - googleUserInfo, err := client.GetUserInfo(token.AccessToken) - if err != nil { - l.Errorw("error get google user info: %v", logger.Field("error", err.Error())) - return nil, err - } - // query user info - userAuthMethod, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, "google", googleUserInfo.OpenID) - if err != nil { - if errors.As(err, &gorm.ErrRecordNotFound) { - return l.register(googleUserInfo.Email, googleUserInfo.Picture, "google", googleUserInfo.OpenID) - } - return nil, err - } - return l.svcCtx.UserModel.FindOne(l.ctx, userAuthMethod.UserId) -} - -func (l *OAuthLoginGetTokenLogic) apple(req *types.OAuthLoginGetTokenRequest) (*user.User, error) { - // validate the state code - _, err := l.svcCtx.Redis.Get(l.ctx, fmt.Sprintf("apple:%s", req.Callback.(map[string]interface{})["state"])).Result() - if err != nil { - l.Errorw("[AppleLoginCallback] Get State code error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "get apple state code failed: %v", err.Error()) - } - appleAuth, err := l.svcCtx.AuthModel.FindOneByMethod(l.ctx, "apple") - if err != nil { - l.Errorw("[AppleLoginCallback] FindOneByMethod error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find apple auth method failed: %v", err.Error()) - } - var appleCfg auth.AppleAuthConfig - err = appleCfg.Unmarshal(appleAuth.Config) - if err != nil { - l.Errorw("[AppleLoginCallback] Unmarshal error", logger.Field("error", err.Error()), logger.Field("config", appleAuth.Config)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "unmarshal apple config failed: %v", err.Error()) - } - - client, err := apple.New(apple.Config{ - ClientID: appleCfg.ClientId, - TeamID: appleCfg.TeamID, - KeyID: appleCfg.KeyID, - ClientSecret: appleCfg.ClientSecret, - RedirectURI: appleCfg.RedirectURL, - }) - if err != nil { - l.Errorw("[AppleLoginCallback] New apple client error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "new apple client failed: %v", err.Error()) - } - // verify web token - resp, err := client.VerifyWebToken(l.ctx, req.Callback.(map[string]interface{})["code"].(string)) - if err != nil { - l.Errorw("[AppleLoginCallback] VerifyWebToken error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "verify web token failed: %v", err.Error()) - } - if resp.Error != "" { - l.Errorw("[AppleLoginCallback] VerifyWebToken error", logger.Field("error", resp.Error)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "verify web token failed: %v", resp.Error) - } - // query apple user unique id - appleUnique, err := apple.GetUniqueID(resp.IDToken) - if err != nil { - l.Errorw("[AppleLoginCallback] GetUniqueID error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "get apple unique id failed: %v", err.Error()) - } - // get apple user info - appleUserInfo, err := apple.GetClaims(resp.AccessToken) - if err != nil { - l.Errorw("[AppleLoginCallback] GetClaims error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "get apple user info failed: %v", err.Error()) - } - // query user by apple unique id - userAuthMethod, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, "apple", appleUnique) - if err != nil { - // if user not exist, handle register - if errors.Is(err, gorm.ErrRecordNotFound) { - return l.register((*appleUserInfo)["email"].(string), "", "apple", appleUnique) - } - l.Errorw("[AppleLoginCallback] FindUserAuthMethodByOpenID error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user auth method by openid failed: %v", err.Error()) - } - // query user info - userInfo, err := l.svcCtx.UserModel.FindOne(l.ctx, userAuthMethod.UserId) - - if err != nil { - l.Errorw( - "[AppleLoginCallback] FindOne error", + l.Errorw("failed to cache session id", + logger.Field("request_id", requestID), + logger.Field("user_id", userInfo.Id), + logger.Field("session_id", sessionId), logger.Field("error", err.Error()), ) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user info failed: %v", err.Error()) + return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err) } - return userInfo, nil + l.Infow("jwt token generated successfully", + logger.Field("request_id", requestID), + logger.Field("user_id", userInfo.Id), + logger.Field("session_id", sessionId), + logger.Field("duration_ms", time.Since(startTime).Milliseconds()), + ) + + return token, nil } -func (l *OAuthLoginGetTokenLogic) telegram(req *types.OAuthLoginGetTokenRequest) (*user.User, error) { - appleAuth, err := l.svcCtx.AuthModel.FindOneByMethod(l.ctx, "telegram") +func (l *OAuthLoginGetTokenLogic) validateStateCode(provider, state, requestID string) (string, error) { + stateKey := fmt.Sprintf("%s:%s", provider, state) + l.Debugw("validating oauth state code", + logger.Field("request_id", requestID), + logger.Field("provider", provider), + logger.Field("state_key", stateKey), + ) + + redirect, err := l.svcCtx.Redis.Get(l.ctx, stateKey).Result() if err != nil { - l.Errorw("[OAuthLoginGetToken] FindOneByMethod error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find telegram auth method failed: %v", err.Error()) + l.Errorw("failed to validate state code", + logger.Field("request_id", requestID), + logger.Field("provider", provider), + logger.Field("state_key", stateKey), + logger.Field("error", err.Error()), + ) + return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "get %s state code failed: %v", provider, err) } - var telegramCfg auth.TelegramAuthConfig - err = json.Unmarshal([]byte(appleAuth.Config), &telegramCfg) + + l.Debugw("state code validated successfully", + logger.Field("request_id", requestID), + logger.Field("provider", provider), + logger.Field("redirect_url", redirect), + ) + return redirect, nil +} + +func (l *OAuthLoginGetTokenLogic) getGoogleConfig(requestID string) (*auth.GoogleAuthConfig, error) { + l.Debugw("fetching google oauth config", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthGoogle), + ) + + authMethod, err := l.svcCtx.AuthModel.FindOneByMethod(l.ctx, OAuthGoogle) if err != nil { - l.Errorw("[OAuthLoginGetToken] Unmarshal error", logger.Field("error", err.Error()), logger.Field("config", appleAuth.Config)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "unmarshal telegram config failed: %v", err.Error()) + l.Errorw("failed to find google auth method", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthGoogle), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find google auth method failed: %v", err) } - encodeText := req.Callback.(map[string]interface{})["tgAuthResult"].(string) - // base64 decode - callbackData, err := telegram.ParseAndValidateBase64([]byte(encodeText), telegramCfg.BotToken) + + var cfg auth.GoogleAuthConfig + if err = cfg.Unmarshal(authMethod.Config); err != nil { + l.Errorw("failed to unmarshal google config", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthGoogle), + logger.Field("config", authMethod.Config), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "unmarshal google config failed: %v", err) + } + + l.Debugw("google oauth config loaded successfully", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthGoogle), + logger.Field("client_id", cfg.ClientId), + ) + return &cfg, nil +} + +func (l *OAuthLoginGetTokenLogic) getAppleConfig(requestID string) (*auth.AppleAuthConfig, error) { + l.Debugw("fetching apple oauth config", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthApple), + ) + + authMethod, err := l.svcCtx.AuthModel.FindOneByMethod(l.ctx, OAuthApple) if err != nil { - l.Errorw("[TelegramLoginCallback] ParseAndValidateBase64 error", logger.Field("error", err.Error())) - return nil, err + l.Errorw("failed to find apple auth method", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthApple), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find apple auth method failed: %v", err) } - // 验证数据有效期 - if time.Now().Unix()-*callbackData.AuthDate > 86400 { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "auth date expired") + + var cfg auth.AppleAuthConfig + if err = cfg.Unmarshal(authMethod.Config); err != nil { + l.Errorw("failed to unmarshal apple config", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthApple), + logger.Field("config", authMethod.Config), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "unmarshal apple config failed: %v", err) } - // query user auth info - userAuthMethod, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, "telegram", fmt.Sprintf("%v", *callbackData.Id)) + + l.Debugw("apple oauth config loaded successfully", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthApple), + logger.Field("client_id", cfg.ClientId), + logger.Field("team_id", cfg.TeamID), + ) + return &cfg, nil +} + +func (l *OAuthLoginGetTokenLogic) getTelegramConfig(requestID string) (*auth.TelegramAuthConfig, error) { + l.Debugw("fetching telegram oauth config", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthTelegram), + ) + + authMethod, err := l.svcCtx.AuthModel.FindOneByMethod(l.ctx, OAuthTelegram) if err != nil { - if errors.As(err, &gorm.ErrRecordNotFound) { - return l.register(fmt.Sprintf("%v@%s", *callbackData.Id, "qq.com"), *callbackData.PhotoUrl, "telegram", fmt.Sprintf("%v", callbackData.Id)) + l.Errorw("failed to find telegram auth method", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthTelegram), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find telegram auth method failed: %v", err) + } + + var cfg auth.TelegramAuthConfig + if err = json.Unmarshal([]byte(authMethod.Config), &cfg); err != nil { + l.Errorw("failed to unmarshal telegram config", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthTelegram), + logger.Field("config", authMethod.Config), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "unmarshal telegram config failed: %v", err) + } + + l.Debugw("telegram oauth config loaded successfully", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthTelegram), + ) + return &cfg, nil +} + +func (l *OAuthLoginGetTokenLogic) findOrRegisterUser(authType, openID, email, avatar, requestID, ip, userAgent string) (*user.User, error) { + l.Debugw("finding or registering user", + logger.Field("request_id", requestID), + logger.Field("auth_type", authType), + logger.Field("openid", openID), + logger.Field("email", email), + ) + + userAuthMethod, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, authType, openID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + l.Infow("user not found, starting registration", + logger.Field("request_id", requestID), + logger.Field("auth_type", authType), + logger.Field("openid", openID), + logger.Field("email", email), + ) + return l.register(email, avatar, authType, openID, requestID, ip, userAgent) } + l.Errorw("failed to find user auth method by openid", + logger.Field("request_id", requestID), + logger.Field("auth_type", authType), + logger.Field("openid", openID), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user auth method by openid failed: %v", err) } - // query user info + + l.Debugw("found existing user auth method", + logger.Field("request_id", requestID), + logger.Field("auth_type", authType), + logger.Field("user_id", userAuthMethod.UserId), + ) + userInfo, err := l.svcCtx.UserModel.FindOne(l.ctx, userAuthMethod.UserId) if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user info failed: %v", err.Error()) + l.Errorw("failed to find user by id", + logger.Field("request_id", requestID), + logger.Field("user_id", userAuthMethod.UserId), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user info failed: %v", err) } + + l.Infow("existing user found successfully", + logger.Field("request_id", requestID), + logger.Field("user_id", userInfo.Id), + logger.Field("auth_type", authType), + ) + return userInfo, nil } -func (l *OAuthLoginGetTokenLogic) register(email, avatar, method, openid string) (*user.User, error) { - if l.svcCtx.Config.Invite.ForcedInvite { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.InviteCodeError), "invite code is required") - } - var userInfo *user.User - err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { - err := db.Model(&user.User{}).Where("email = ?", email).First(&userInfo).Error - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return err - } - if userInfo.Id != 0 { - return errors.Wrapf(xerr.NewErrCode(xerr.UserExist), "user email exist: %v", email) - } - userInfo = &user.User{ - Avatar: avatar, - } - if err := db.Create(userInfo).Error; err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create user info failed: %v", err.Error()) - } - // Generate ReferCode - userInfo.ReferCode = uuidx.UserInviteCode(userInfo.Id) - // Update ReferCode - err = db.Where("id = ?", userInfo.Id).Update("refer_code", userInfo.ReferCode).Error - if err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update refer code failed: %v", err.Error()) - } - authMethod := &user.AuthMethods{ - UserId: userInfo.Id, - AuthType: method, - AuthIdentifier: openid, - Verified: true, - } - if err = db.Create(authMethod).Error; err != nil { - l.Errorw("error create auth method: %v", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create auth method failed: %v", err.Error()) - } - if email != "" { - authMethod = &user.AuthMethods{ - UserId: userInfo.Id, - AuthType: "email", - AuthIdentifier: email, - Verified: true, - } - if err := db.Create(authMethod).Error; err != nil { - l.Errorw("error create auth method: %v", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create auth method failed: %v", err.Error()) - } - } - if l.svcCtx.Config.Register.EnableTrial { - // Active trial - if err = l.activeTrial(userInfo.Id); err != nil { - return err - } - } - return nil - }) - return userInfo, err -} +func (l *OAuthLoginGetTokenLogic) activeTrial(uid int64, requestID string) error { + l.Debugw("fetching trial subscription template", + logger.Field("request_id", requestID), + logger.Field("user_id", uid), + logger.Field("trial_subscribe_id", l.svcCtx.Config.Register.TrialSubscribe), + ) -func (l *OAuthLoginGetTokenLogic) activeTrial(uid int64) error { sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, l.svcCtx.Config.Register.TrialSubscribe) if err != nil { + l.Errorw("failed to find trial subscription template", + logger.Field("request_id", requestID), + logger.Field("user_id", uid), + logger.Field("trial_subscribe_id", l.svcCtx.Config.Register.TrialSubscribe), + logger.Field("error", err.Error()), + ) return err } + + startTime := time.Now() + expireTime := tool.AddTime(l.svcCtx.Config.Register.TrialTimeUnit, l.svcCtx.Config.Register.TrialTime, startTime) + subscribeToken := uuidx.SubscribeToken(fmt.Sprintf("Trial-%v", uid)) + subscribeUUID := uuidx.NewUUID().String() + + l.Debugw("creating trial subscription", + logger.Field("request_id", requestID), + logger.Field("user_id", uid), + logger.Field("subscribe_id", sub.Id), + logger.Field("start_time", startTime), + logger.Field("expire_time", expireTime), + logger.Field("traffic", sub.Traffic), + logger.Field("token", subscribeToken), + logger.Field("uuid", subscribeUUID), + ) + userSub := &user.Subscribe{ Id: 0, UserId: uid, OrderId: 0, SubscribeId: sub.Id, - StartTime: time.Now(), - ExpireTime: tool.AddTime(l.svcCtx.Config.Register.TrialTimeUnit, l.svcCtx.Config.Register.TrialTime, time.Now()), + StartTime: startTime, + ExpireTime: expireTime, Traffic: sub.Traffic, Download: 0, Upload: 0, - Token: uuidx.SubscribeToken(fmt.Sprintf("Trial-%v", uid)), - UUID: uuidx.NewUUID().String(), + Token: subscribeToken, + UUID: subscribeUUID, Status: 1, } - return l.svcCtx.UserModel.InsertSubscribe(l.ctx, userSub) + + if err := l.svcCtx.UserModel.InsertSubscribe(l.ctx, userSub); err != nil { + l.Errorw("failed to insert trial subscription", + logger.Field("request_id", requestID), + logger.Field("user_id", uid), + logger.Field("error", err.Error()), + ) + return err + } + + l.Infow("trial subscription activated successfully", + logger.Field("request_id", requestID), + logger.Field("user_id", uid), + logger.Field("subscribe_id", sub.Id), + logger.Field("expire_time", expireTime), + logger.Field("traffic", sub.Traffic), + ) + return nil } diff --git a/internal/logic/auth/resetPasswordLogic.go b/internal/logic/auth/resetPasswordLogic.go index b489606..aef245a 100644 --- a/internal/logic/auth/resetPasswordLogic.go +++ b/internal/logic/auth/resetPasswordLogic.go @@ -6,6 +6,7 @@ import ( "fmt" "time" + "github.com/perfect-panel/server/internal/model/log" "github.com/perfect-panel/server/internal/model/user" "github.com/perfect-panel/server/pkg/jwt" "github.com/perfect-panel/server/pkg/uuidx" @@ -43,13 +44,26 @@ func (l *ResetPasswordLogic) ResetPassword(req *types.ResetPasswordRequest) (res defer func() { if userInfo.Id != 0 && loginStatus { - if err := l.svcCtx.UserModel.InsertLoginLog(l.ctx, &user.LoginLog{ - UserId: userInfo.Id, + loginLog := log.Login{ + Method: "email", LoginIP: req.IP, UserAgent: req.UserAgent, - Success: &loginStatus, + Success: loginStatus, + Timestamp: time.Now().UnixMilli(), + } + content, _ := loginLog.Marshal() + if err := l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + Id: 0, + Type: log.TypeLogin.Uint8(), + Date: time.Now().Format("2006-01-02"), + ObjectID: userInfo.Id, + Content: string(content), }); err != nil { - l.Logger.Error("[ResetPassword] insert login log error", logger.Field("error", err.Error())) + l.Errorw("failed to insert login log", + logger.Field("user_id", userInfo.Id), + logger.Field("ip", req.IP), + logger.Field("error", err.Error()), + ) } } }() @@ -90,9 +104,26 @@ func (l *ResetPasswordLogic) ResetPassword(req *types.ResetPasswordRequest) (res // Update password userInfo.Password = tool.EncodePassWord(req.Password) - if err := l.svcCtx.UserModel.Update(l.ctx, userInfo); err != nil { + userInfo.Algo = "default" + if err = l.svcCtx.UserModel.Update(l.ctx, userInfo); err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update user info failed: %v", err.Error()) } + + // Bind device to user if identifier is provided + if req.Identifier != "" { + bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx) + if err := bindLogic.BindDeviceToUser(req.Identifier, req.IP, req.UserAgent, userInfo.Id); err != nil { + l.Errorw("failed to bind device to user", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + // Don't fail register if device binding fails, just log the error + } + } + if l.ctx.Value(constant.LoginType) != nil { + req.LoginType = l.ctx.Value(constant.LoginType).(string) + } // Generate session id sessionId := uuidx.NewUUID().String() // Generate token @@ -102,6 +133,7 @@ func (l *ResetPasswordLogic) ResetPassword(req *types.ResetPasswordRequest) (res l.svcCtx.Config.JwtAuth.AccessExpire, jwt.WithOption("UserId", userInfo.Id), jwt.WithOption("SessionId", sessionId), + jwt.WithOption("LoginType", req.LoginType), ) if err != nil { l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error())) diff --git a/internal/logic/auth/telephoneLoginLogic.go b/internal/logic/auth/telephoneLoginLogic.go index 5457b39..8a54ff5 100644 --- a/internal/logic/auth/telephoneLoginLogic.go +++ b/internal/logic/auth/telephoneLoginLogic.go @@ -9,6 +9,7 @@ import ( "github.com/perfect-panel/server/internal/config" "github.com/perfect-panel/server/internal/logic/common" + "github.com/perfect-panel/server/internal/model/log" "github.com/perfect-panel/server/internal/model/user" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" @@ -51,13 +52,26 @@ func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r // Record login status defer func(svcCtx *svc.ServiceContext) { if userInfo.Id != 0 { - if err := svcCtx.UserModel.InsertLoginLog(l.ctx, &user.LoginLog{ - UserId: userInfo.Id, + loginLog := log.Login{ + Method: "mobile", LoginIP: ip, UserAgent: r.UserAgent(), - Success: &loginStatus, + Success: loginStatus, + Timestamp: time.Now().UnixMilli(), + } + content, _ := loginLog.Marshal() + if err := l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + Id: 0, + Type: log.TypeLogin.Uint8(), + Date: time.Now().Format("2006-01-02"), + ObjectID: userInfo.Id, + Content: string(content), }); err != nil { - l.Logger.Error("[UserLogin] insert login log error", logger.Field("error", err.Error())) + l.Errorw("failed to insert login log", + logger.Field("user_id", userInfo.Id), + logger.Field("ip", req.IP), + logger.Field("error", err.Error()), + ) } } }(l.svcCtx) @@ -84,7 +98,7 @@ func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r if req.TelephoneCode == "" { // Verify password - if !tool.VerifyPassWord(req.Password, userInfo.Password) { + if !tool.MultiPasswordVerify(userInfo.Algo, userInfo.Salt, req.Password, userInfo.Password) { return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserPasswordError), "user password") } } else { @@ -110,6 +124,23 @@ func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r l.svcCtx.Redis.Del(l.ctx, cacheKey) } + // Bind device to user if identifier is provided + if req.Identifier != "" { + bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx) + if err := bindLogic.BindDeviceToUser(req.Identifier, req.IP, req.UserAgent, userInfo.Id); err != nil { + l.Errorw("failed to bind device to user", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + // Don't fail login if device binding fails, just log the error + } + } + + if l.ctx.Value(constant.LoginType) != nil { + req.LoginType = l.ctx.Value(constant.LoginType).(string) + } + // Generate session id sessionId := uuidx.NewUUID().String() // Generate token @@ -119,6 +150,7 @@ func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r l.svcCtx.Config.JwtAuth.AccessExpire, jwt.WithOption("UserId", userInfo.Id), jwt.WithOption("SessionId", sessionId), + jwt.WithOption("LoginType", req.LoginType), ) if err != nil { l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error())) diff --git a/internal/logic/auth/telephoneResetPasswordLogic.go b/internal/logic/auth/telephoneResetPasswordLogic.go index 972b9ae..5cb47cc 100644 --- a/internal/logic/auth/telephoneResetPasswordLogic.go +++ b/internal/logic/auth/telephoneResetPasswordLogic.go @@ -6,6 +6,7 @@ import ( "time" "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/model/log" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/constant" @@ -77,11 +78,27 @@ func (l *TelephoneResetPasswordLogic) TelephoneResetPassword(req *types.Telephon // Generate password pwd := tool.EncodePassWord(req.Password) userInfo.Password = pwd + userInfo.Algo = "default" err = l.svcCtx.UserModel.Update(l.ctx, userInfo) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "update user password failed: %v", err.Error()) } + // Bind device to user if identifier is provided + if req.Identifier != "" { + bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx) + if err := bindLogic.BindDeviceToUser(req.Identifier, req.IP, req.UserAgent, userInfo.Id); err != nil { + l.Errorw("failed to bind device to user", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + // Don't fail register if device binding fails, just log the error + } + } + if l.ctx.Value(constant.LoginType) != nil { + req.LoginType = l.ctx.Value(constant.LoginType).(string) + } // Generate session id sessionId := uuidx.NewUUID().String() // Generate token @@ -91,6 +108,7 @@ func (l *TelephoneResetPasswordLogic) TelephoneResetPassword(req *types.Telephon l.svcCtx.Config.JwtAuth.AccessExpire, jwt.WithOption("UserId", userInfo.Id), jwt.WithOption("SessionId", sessionId), + jwt.WithOption("LoginType", req.LoginType), ) if err != nil { l.Errorw("[UserLogin] token generate error", logger.Field("error", err.Error())) @@ -100,6 +118,31 @@ func (l *TelephoneResetPasswordLogic) TelephoneResetPassword(req *types.Telephon if err = l.svcCtx.Redis.Set(l.ctx, sessionIdCacheKey, userInfo.Id, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err.Error()) } + defer func() { + if token != "" && userInfo.Id != 0 { + loginLog := log.Login{ + Method: "mobile", + LoginIP: req.IP, + UserAgent: req.UserAgent, + Success: token != "", + Timestamp: time.Now().UnixMilli(), + } + content, _ := loginLog.Marshal() + if err := l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + Id: 0, + Type: log.TypeLogin.Uint8(), + Date: time.Now().Format("2006-01-02"), + ObjectID: userInfo.Id, + Content: string(content), + }); err != nil { + l.Errorw("failed to insert login log", + logger.Field("user_id", userInfo.Id), + logger.Field("ip", req.IP), + logger.Field("error", err.Error()), + ) + } + } + }() return &types.LoginResponse{ Token: token, }, nil diff --git a/internal/logic/auth/telephoneUserRegisterLogic.go b/internal/logic/auth/telephoneUserRegisterLogic.go index 0ff2c6d..af16811 100644 --- a/internal/logic/auth/telephoneUserRegisterLogic.go +++ b/internal/logic/auth/telephoneUserRegisterLogic.go @@ -6,6 +6,7 @@ import ( "fmt" "time" + "github.com/perfect-panel/server/internal/model/log" "github.com/perfect-panel/server/pkg/constant" "github.com/perfect-panel/server/internal/config" @@ -105,7 +106,9 @@ func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneR // Generate password pwd := tool.EncodePassWord(req.Password) userInfo := &user.User{ - Password: pwd, + Password: pwd, + Algo: "default", + OnlyFirstPurchase: &l.svcCtx.Config.Invite.OnlyFirstPurchase, AuthMethods: []user.AuthMethods{ { AuthType: "mobile", @@ -136,6 +139,22 @@ func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneR } return nil }) + + // Bind device to user if identifier is provided + if req.Identifier != "" { + bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx) + if err := bindLogic.BindDeviceToUser(req.Identifier, req.IP, req.UserAgent, userInfo.Id); err != nil { + l.Errorw("failed to bind device to user", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + // Don't fail register if device binding fails, just log the error + } + } + if l.ctx.Value(constant.LoginType) != nil { + req.LoginType = l.ctx.Value(constant.LoginType).(string) + } // Generate session id sessionId := uuidx.NewUUID().String() // Generate token @@ -145,6 +164,7 @@ func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneR l.svcCtx.Config.JwtAuth.AccessExpire, jwt.WithOption("UserId", userInfo.Id), jwt.WithOption("SessionId", sessionId), + jwt.WithOption("LoginType", req.LoginType), ) if err != nil { l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error())) @@ -154,6 +174,53 @@ func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneR if err = l.svcCtx.Redis.Set(l.ctx, sessionIdCacheKey, userInfo.Id, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err.Error()) } + + defer func() { + if token != "" && userInfo.Id != 0 { + loginLog := log.Login{ + Method: "mobile", + LoginIP: req.IP, + UserAgent: req.UserAgent, + Success: token != "", + Timestamp: time.Now().UnixMilli(), + } + content, _ := loginLog.Marshal() + if err := l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + Id: 0, + Type: log.TypeLogin.Uint8(), + Date: time.Now().Format("2006-01-02"), + ObjectID: userInfo.Id, + Content: string(content), + }); err != nil { + l.Errorw("failed to insert login log", + logger.Field("user_id", userInfo.Id), + logger.Field("ip", req.IP), + logger.Field("error", err.Error()), + ) + } + + // Register log + registerLog := log.Register{ + AuthMethod: "mobile", + Identifier: phoneNumber, + RegisterIP: req.IP, + UserAgent: req.UserAgent, + Timestamp: time.Now().UnixMilli(), + } + content, _ = registerLog.Marshal() + if err := l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + Type: log.TypeRegister.Uint8(), + ObjectID: userInfo.Id, + Date: time.Now().Format("2006-01-02"), + Content: string(content), + }); err != nil { + l.Errorw("failed to insert login log", + logger.Field("user_id", userInfo.Id), + logger.Field("ip", req.IP), + logger.Field("error", err.Error())) + } + } + }() return &types.LoginResponse{ Token: token, }, nil diff --git a/internal/logic/auth/userLoginLogic.go b/internal/logic/auth/userLoginLogic.go index c04392c..deecaae 100644 --- a/internal/logic/auth/userLoginLogic.go +++ b/internal/logic/auth/userLoginLogic.go @@ -5,6 +5,8 @@ import ( "fmt" "time" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/pkg/constant" "github.com/perfect-panel/server/pkg/logger" "github.com/perfect-panel/server/internal/config" @@ -41,29 +43,59 @@ func (l *UserLoginLogic) UserLogin(req *types.UserLoginRequest) (resp *types.Log // Record login status defer func(svcCtx *svc.ServiceContext) { if userInfo.Id != 0 { - if err := svcCtx.UserModel.InsertLoginLog(l.ctx, &user.LoginLog{ - UserId: userInfo.Id, + loginLog := log.Login{ + Method: "email", LoginIP: req.IP, UserAgent: req.UserAgent, - Success: &loginStatus, + Success: loginStatus, + Timestamp: time.Now().UnixMilli(), + } + content, _ := loginLog.Marshal() + if err := l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + Type: log.TypeLogin.Uint8(), + Date: time.Now().Format("2006-01-02"), + ObjectID: userInfo.Id, + Content: string(content), }); err != nil { - l.Logger.Error("[UserLogin] insert login log error", logger.Field("error", err.Error())) + l.Errorw("failed to insert login log", + logger.Field("user_id", userInfo.Id), + logger.Field("ip", req.IP), + logger.Field("error", err.Error()), + ) } } }(l.svcCtx) userInfo, err = l.svcCtx.UserModel.FindOneByEmail(l.ctx, req.Email) + if err != nil { if errors.As(err, &gorm.ErrRecordNotFound) { - logger.WithContext(l.ctx).Error(err) return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "user email not exist: %v", req.Email) } + logger.WithContext(l.ctx).Error(err) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user info failed: %v", err.Error()) } + // Verify password - if !tool.VerifyPassWord(req.Password, userInfo.Password) { + if !tool.MultiPasswordVerify(userInfo.Algo, userInfo.Salt, req.Password, userInfo.Password) { return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserPasswordError), "user password") } + + // Bind device to user if identifier is provided + if req.Identifier != "" { + bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx) + if err := bindLogic.BindDeviceToUser(req.Identifier, req.IP, req.UserAgent, userInfo.Id); err != nil { + l.Errorw("failed to bind device to user", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + // Don't fail login if device binding fails, just log the error + } + } + if l.ctx.Value(constant.LoginType) != nil { + req.LoginType = l.ctx.Value(constant.LoginType).(string) + } // Generate session id sessionId := uuidx.NewUUID().String() // Generate token @@ -73,6 +105,7 @@ func (l *UserLoginLogic) UserLogin(req *types.UserLoginRequest) (resp *types.Log l.svcCtx.Config.JwtAuth.AccessExpire, jwt.WithOption("UserId", userInfo.Id), jwt.WithOption("SessionId", sessionId), + jwt.WithOption("LoginType", req.LoginType), ) if err != nil { l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error())) diff --git a/internal/logic/auth/userRegisterLogic.go b/internal/logic/auth/userRegisterLogic.go index 19c2282..cf959a9 100644 --- a/internal/logic/auth/userRegisterLogic.go +++ b/internal/logic/auth/userRegisterLogic.go @@ -8,6 +8,7 @@ import ( "github.com/perfect-panel/server/internal/config" "github.com/perfect-panel/server/internal/logic/common" + "github.com/perfect-panel/server/internal/model/log" "github.com/perfect-panel/server/internal/model/user" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" @@ -88,7 +89,9 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp * // Generate password pwd := tool.EncodePassWord(req.Password) userInfo := &user.User{ - Password: pwd, + Password: pwd, + Algo: "default", + OnlyFirstPurchase: &l.svcCtx.Config.Invite.OnlyFirstPurchase, } if referer != nil { userInfo.RefererId = referer.Id @@ -123,6 +126,21 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp * } return nil }) + // Bind device to user if identifier is provided + if req.Identifier != "" { + bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx) + if err := bindLogic.BindDeviceToUser(req.Identifier, req.IP, req.UserAgent, userInfo.Id); err != nil { + l.Errorw("failed to bind device to user", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + // Don't fail register if device binding fails, just log the error + } + } + if l.ctx.Value(constant.LoginType) != nil { + req.LoginType = l.ctx.Value(constant.LoginType).(string) + } // Generate session id sessionId := uuidx.NewUUID().String() // Generate token @@ -132,6 +150,7 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp * l.svcCtx.Config.JwtAuth.AccessExpire, jwt.WithOption("UserId", userInfo.Id), jwt.WithOption("SessionId", sessionId), + jwt.WithOption("LoginType", req.LoginType), ) if err != nil { l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error())) @@ -145,13 +164,47 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp * loginStatus := true defer func() { if token != "" && userInfo.Id != 0 { - if err := l.svcCtx.UserModel.InsertLoginLog(l.ctx, &user.LoginLog{ - UserId: userInfo.Id, + loginLog := log.Login{ + Method: "email", LoginIP: req.IP, UserAgent: req.UserAgent, - Success: &loginStatus, + Success: loginStatus, + Timestamp: time.Now().UnixMilli(), + } + content, _ := loginLog.Marshal() + if err := l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + Id: 0, + Type: log.TypeLogin.Uint8(), + Date: time.Now().Format("2006-01-02"), + ObjectID: userInfo.Id, + Content: string(content), }); err != nil { - l.Logger.Error("[UserRegister] insert login log error", logger.Field("error", err.Error())) + l.Errorw("failed to insert login log", + logger.Field("user_id", userInfo.Id), + logger.Field("ip", req.IP), + logger.Field("error", err.Error()), + ) + } + + // Register log + registerLog := log.Register{ + AuthMethod: "email", + Identifier: req.Email, + RegisterIP: req.IP, + UserAgent: req.UserAgent, + Timestamp: time.Now().UnixMilli(), + } + content, _ = registerLog.Marshal() + if err = l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + Type: log.TypeRegister.Uint8(), + ObjectID: userInfo.Id, + Date: time.Now().Format("2006-01-02"), + Content: string(content), + }); err != nil { + l.Errorw("failed to insert login log", + logger.Field("user_id", userInfo.Id), + logger.Field("ip", req.IP), + logger.Field("error", err.Error())) } } }() diff --git a/internal/logic/common/getApplicationLogic.go b/internal/logic/common/getApplicationLogic.go deleted file mode 100644 index 7a4b6d6..0000000 --- a/internal/logic/common/getApplicationLogic.go +++ /dev/null @@ -1,136 +0,0 @@ -package common - -import ( - "context" - "strings" - - "github.com/perfect-panel/server/internal/model/application" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" - "gorm.io/gorm" - - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" -) - -type GetApplicationLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Get Tos Content -func NewGetApplicationLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetApplicationLogic { - return &GetApplicationLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *GetApplicationLogic) GetApplication() (resp *types.GetAppcationResponse, err error) { - resp = &types.GetAppcationResponse{} - - cfg, err := l.svcCtx.ApplicationModel.FindOneConfig(l.ctx, 1) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - l.Logger.Error("[GetAppInfo] FindOneAppConfig error: ", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetAppInfo FindOneAppConfig error: %v", err.Error()) - } - if err != nil { - resp.Config = types.ApplicationConfig{} - } else { - resp.Config = types.ApplicationConfig{ - AppId: cfg.AppId, - EncryptionKey: cfg.EncryptionKey, - EncryptionMethod: cfg.EncryptionMethod, - Domains: strings.Split(cfg.Domains, ";"), - StartupPicture: cfg.StartupPicture, - StartupPictureSkipTime: cfg.StartupPictureSkipTime, - } - } - - var applications []*application.Application - err = l.svcCtx.ApplicationModel.Transaction(l.ctx, func(tx *gorm.DB) (err error) { - return tx.Model(applications).Preload("ApplicationVersions").Find(&applications).Error - }) - if err != nil { - l.Errorw("[QueryApplicationConfig] get application error: ", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get application error: %v", err.Error()) - } - - if len(applications) == 0 { - return resp, nil - } - - for _, app := range applications { - applicationResponse := types.ApplicationResponseInfo{ - Id: app.Id, - Name: app.Name, - Icon: app.Icon, - Description: app.Description, - SubscribeType: app.SubscribeType, - } - applicationVersions := app.ApplicationVersions - if len(applicationVersions) != 0 { - for _, applicationVersion := range applicationVersions { - /*if !applicationVersion.IsDefault { - continue - }*/ - switch applicationVersion.Platform { - case "ios": - applicationResponse.Platform.IOS = append(applicationResponse.Platform.IOS, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - case "macos": - applicationResponse.Platform.MacOS = append(applicationResponse.Platform.MacOS, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - case "linux": - applicationResponse.Platform.Linux = append(applicationResponse.Platform.Linux, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - case "android": - applicationResponse.Platform.Android = append(applicationResponse.Platform.Android, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - case "windows": - applicationResponse.Platform.Windows = append(applicationResponse.Platform.Windows, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - case "harmony": - applicationResponse.Platform.Harmony = append(applicationResponse.Platform.Harmony, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - } - } - } - resp.Applications = append(resp.Applications, applicationResponse) - } - - return -} diff --git a/internal/logic/common/getClientLogic.go b/internal/logic/common/getClientLogic.go new file mode 100644 index 0000000..7938de1 --- /dev/null +++ b/internal/logic/common/getClientLogic.go @@ -0,0 +1,56 @@ +package common + +import ( + "context" + "encoding/json" + + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetClientLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get Client +func NewGetClientLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetClientLogic { + return &GetClientLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetClientLogic) GetClient() (resp *types.GetSubscribeClientResponse, err error) { + data, err := l.svcCtx.ClientModel.List(l.ctx) + if err != nil { + l.Errorf("Failed to get subscribe application list: %v", err) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Failed to get subscribe application list") + } + var list []types.SubscribeClient + for _, item := range data { + var temp types.DownloadLink + if item.DownloadLink != "" { + _ = json.Unmarshal([]byte(item.DownloadLink), &temp) + } + list = append(list, types.SubscribeClient{ + Id: item.Id, + Name: item.Name, + Description: item.Description, + Icon: item.Icon, + Scheme: item.Scheme, + IsDefault: item.IsDefault, + DownloadLink: temp, + }) + } + resp = &types.GetSubscribeClientResponse{ + Total: int64(len(list)), + List: list, + } + return +} diff --git a/internal/logic/common/getGlobalConfigLogic.go b/internal/logic/common/getGlobalConfigLogic.go index 694520d..61b2c1e 100644 --- a/internal/logic/common/getGlobalConfigLogic.go +++ b/internal/logic/common/getGlobalConfigLogic.go @@ -2,6 +2,7 @@ package common import ( "context" + "encoding/json" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" @@ -67,6 +68,10 @@ func (l *GetGlobalConfigLogic) GetGlobalConfig() (resp *types.GetGlobalConfigRes for _, method := range authMethods { if *method.Enabled { methods = append(methods, method.Method) + if method.Method == "device" { + _ = json.Unmarshal([]byte(method.Config), &resp.Auth.Device) + resp.Auth.Device.Enable = true + } } } resp.OAuthMethods = methods diff --git a/internal/logic/common/getSubscriptionLogic.go b/internal/logic/common/getSubscriptionLogic.go deleted file mode 100644 index 463abfe..0000000 --- a/internal/logic/common/getSubscriptionLogic.go +++ /dev/null @@ -1,41 +0,0 @@ -package common - -import ( - "context" - - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/tool" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" -) - -type GetSubscriptionLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Get Subscription -func NewGetSubscriptionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetSubscriptionLogic { - return &GetSubscriptionLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *GetSubscriptionLogic) GetSubscription() (resp *types.GetSubscriptionResponse, err error) { - resp = &types.GetSubscriptionResponse{ - List: make([]types.Subscribe, 0), - } - // Get the subscription list - data, err := l.svcCtx.SubscribeModel.QuerySubscribeListByShow(l.ctx) - if err != nil { - l.Errorw("[Site GetSubscription]", logger.Field("err", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get subscription list error: %v", err.Error()) - } - tool.DeepCopy(&resp.List, data) - return -} diff --git a/internal/logic/common/heartbeatLogic.go b/internal/logic/common/heartbeatLogic.go new file mode 100644 index 0000000..1bfb081 --- /dev/null +++ b/internal/logic/common/heartbeatLogic.go @@ -0,0 +1,33 @@ +package common + +import ( + "context" + "time" + + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" +) + +type HeartbeatLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewHeartbeatLogic Heartbeat +func NewHeartbeatLogic(ctx context.Context, svcCtx *svc.ServiceContext) *HeartbeatLogic { + return &HeartbeatLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *HeartbeatLogic) Heartbeat() (resp *types.HeartbeatResponse, err error) { + return &types.HeartbeatResponse{ + Status: true, + Message: "service is alive", + Timestamp: time.Now().Unix(), + }, nil +} diff --git a/internal/logic/common/sendEmailCodeLogic.go b/internal/logic/common/sendEmailCodeLogic.go index ac37acc..c538d72 100644 --- a/internal/logic/common/sendEmailCodeLogic.go +++ b/internal/logic/common/sendEmailCodeLogic.go @@ -1,11 +1,9 @@ package common import ( - "bytes" "context" "encoding/json" "fmt" - "text/template" "time" "github.com/hibiken/asynq" @@ -88,14 +86,16 @@ func (l *SendEmailCodeLogic) SendEmailCode(req *types.SendCodeRequest) (resp *ty var taskPayload queue.SendEmailPayload // Generate verification code code := random.Key(6, 0) + taskPayload.Type = queue.EmailTypeVerify taskPayload.Email = req.Email taskPayload.Subject = "Verification code" - content, err := l.initTemplate(req.Type, code) - if err != nil { - l.Logger.Error("[SendEmailCode]: InitTemplate Error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Failed to init template") + taskPayload.Content = map[string]interface{}{ + "Type": req.Type, + "SiteLogo": l.svcCtx.Config.Site.SiteLogo, + "SiteName": l.svcCtx.Config.Site.SiteName, + "Expire": 5, + "Code": code, } - taskPayload.Content = content // Save to Redis payload = CacheKeyPayload{ Code: code, @@ -134,23 +134,3 @@ func (l *SendEmailCodeLogic) SendEmailCode(req *types.SendCodeRequest) (resp *ty }, nil } } - -func (l *SendEmailCodeLogic) initTemplate(t uint8, code string) (string, error) { - data := VerifyTemplate{ - Type: t, - SiteLogo: l.svcCtx.Config.Site.SiteLogo, - SiteName: l.svcCtx.Config.Site.SiteName, - Expire: 5, - Code: code, - } - tpl, err := template.New("verify").Parse(l.svcCtx.Config.Email.VerifyEmailTemplate) - if err != nil { - return "", err - } - var result bytes.Buffer - err = tpl.Execute(&result, data) - if err != nil { - return "", err - } - return result.String(), nil -} diff --git a/internal/logic/notify/ePayNotifyLogic.go b/internal/logic/notify/ePayNotifyLogic.go index 8def591..efdd127 100644 --- a/internal/logic/notify/ePayNotifyLogic.go +++ b/internal/logic/notify/ePayNotifyLogic.go @@ -57,7 +57,7 @@ func (l *EPayNotifyLogic) EPayNotify(req *types.EPayNotifyRequest) error { return err } // Verify sign - client := epay.NewClient(config.Pid, config.Url, config.Key) + client := epay.NewClient(config.Pid, config.Url, config.Key, config.Type) if !client.VerifySign(urlParamsToMap(l.ctx.Request.URL.RawQuery)) && !l.svcCtx.Config.Debug { l.Logger.Error("[EPayNotify] Verify sign failed") return nil diff --git a/internal/logic/public/order/closeOrderLogic.go b/internal/logic/public/order/closeOrderLogic.go index fc22eec..ced53b8 100644 --- a/internal/logic/public/order/closeOrderLogic.go +++ b/internal/logic/public/order/closeOrderLogic.go @@ -3,7 +3,9 @@ package order import ( "context" "encoding/json" + "time" + "github.com/perfect-panel/server/internal/model/log" "github.com/perfect-panel/server/internal/model/user" "github.com/perfect-panel/server/pkg/payment/stripe" "gorm.io/gorm" @@ -92,15 +94,25 @@ func (l *CloseOrderLogic) CloseOrder(req *types.CloseOrderRequest) error { return err } // Record the deduction refund log - giftAmountLog := &user.GiftAmountLog{ - UserId: orderInfo.UserId, - OrderNo: orderInfo.OrderNo, - Amount: orderInfo.GiftAmount, - Type: 1, - Balance: deduction, - Remark: "Order cancellation refund", + + giftLog := log.Gift{ + Type: log.GiftTypeIncrease, + OrderNo: orderInfo.OrderNo, + SubscribeId: 0, + Amount: orderInfo.GiftAmount, + Balance: deduction, + Remark: "Order cancellation refund", + Timestamp: time.Now().UnixMilli(), } - err = tx.Model(&user.GiftAmountLog{}).Create(giftAmountLog).Error + content, _ := giftLog.Marshal() + + err = tx.Model(&log.SystemLog{}).Create(&log.SystemLog{ + Id: 0, + Type: log.TypeGift.Uint8(), + Date: time.Now().Format(time.DateOnly), + ObjectID: userInfo.Id, + Content: string(content), + }).Error if err != nil { l.Errorw("[CloseOrder] Record cancellation refund log failed", logger.Field("error", err.Error()), diff --git a/internal/logic/public/order/purchaseLogic.go b/internal/logic/public/order/purchaseLogic.go index cabeecc..519a80a 100644 --- a/internal/logic/public/order/purchaseLogic.go +++ b/internal/logic/public/order/purchaseLogic.go @@ -5,6 +5,7 @@ import ( "encoding/json" "time" + "github.com/perfect-panel/server/internal/model/log" "github.com/perfect-panel/server/pkg/constant" "github.com/hibiken/asynq" @@ -113,7 +114,7 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P } return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find coupon error: %v", err.Error()) } - if couponInfo.Count <= couponInfo.UsedCount { + if couponInfo.Count != 0 && couponInfo.Count <= couponInfo.UsedCount { return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponInsufficientUsage), "coupon used") } couponSub := tool.StringToInt64Slice(couponInfo.Subscribe) @@ -159,6 +160,7 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P // Calculate the handling fee if amount > 0 { feeAmount = calculateFee(amount, payment) + amount += feeAmount } // query user is new purchase or renewal isNew, err := l.svcCtx.OrderModel.IsUserEligibleForNewOrder(l.ctx, u.Id) @@ -195,18 +197,26 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P return e } // create deduction record - giftAmountLog := user.GiftAmountLog{ - UserId: orderInfo.UserId, - OrderNo: orderInfo.OrderNo, - Amount: orderInfo.GiftAmount, - Type: 2, - Balance: u.GiftAmount, - Remark: "Purchase order deduction", + giftLog := log.Gift{ + Type: log.GiftTypeReduce, + OrderNo: orderInfo.OrderNo, + SubscribeId: 0, + Amount: orderInfo.GiftAmount, + Balance: u.GiftAmount, + Remark: "Purchase order deduction", + Timestamp: time.Now().UnixMilli(), } - if e := db.Model(&user.GiftAmountLog{}).Create(&giftAmountLog).Error; e != nil { + content, _ := giftLog.Marshal() + + if e := db.Model(&log.SystemLog{}).Create(&log.SystemLog{ + Type: log.TypeGift.Uint8(), + Date: time.Now().Format(time.DateOnly), + ObjectID: u.Id, + Content: string(content), + }).Error; e != nil { l.Errorw("[Purchase] Database insert error", logger.Field("error", e.Error()), - logger.Field("deductionLog", giftAmountLog), + logger.Field("deductionLog", giftLog), ) return e } diff --git a/internal/logic/public/order/renewalLogic.go b/internal/logic/public/order/renewalLogic.go index 6692628..c78824f 100644 --- a/internal/logic/public/order/renewalLogic.go +++ b/internal/logic/public/order/renewalLogic.go @@ -5,6 +5,7 @@ import ( "encoding/json" "time" + "github.com/perfect-panel/server/internal/model/log" "github.com/perfect-panel/server/pkg/constant" "gorm.io/gorm" @@ -83,7 +84,7 @@ func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.Rene } return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find coupon error: %v", err.Error()) } - if couponInfo.Count <= couponInfo.UsedCount { + if couponInfo.Count != 0 && couponInfo.Count <= couponInfo.UsedCount { return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponInsufficientUsage), "coupon used") } couponSub := tool.StringToInt64Slice(couponInfo.Subscribe) @@ -163,16 +164,24 @@ func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.Rene return err } // create deduction record - deductionLog := user.GiftAmountLog{ - UserId: orderInfo.UserId, - OrderNo: orderInfo.OrderNo, - Amount: orderInfo.GiftAmount, - Type: 2, - Balance: u.GiftAmount, - Remark: "Renewal order deduction", + giftLog := log.Gift{ + Type: log.GiftTypeReduce, + OrderNo: orderInfo.OrderNo, + SubscribeId: 0, + Amount: orderInfo.GiftAmount, + Balance: u.GiftAmount, + Remark: "Renewal order deduction", + Timestamp: time.Now().UnixMilli(), } - if err := db.Model(&user.GiftAmountLog{}).Create(&deductionLog).Error; err != nil { - l.Errorw("[Renewal] Database insert error", logger.Field("error", err.Error()), logger.Field("deductionLog", deductionLog)) + content, _ := giftLog.Marshal() + + if err := db.Model(&log.SystemLog{}).Create(&log.SystemLog{ + Type: log.TypeGift.Uint8(), + Date: time.Now().Format(time.DateOnly), + ObjectID: u.Id, + Content: string(content), + }).Error; err != nil { + l.Errorw("[Renewal] Database insert error", logger.Field("error", err.Error()), logger.Field("deductionLog", giftLog)) return err } } diff --git a/internal/logic/public/order/resetTrafficLogic.go b/internal/logic/public/order/resetTrafficLogic.go index 03ec1a0..1fc9b57 100644 --- a/internal/logic/public/order/resetTrafficLogic.go +++ b/internal/logic/public/order/resetTrafficLogic.go @@ -5,6 +5,7 @@ import ( "encoding/json" "time" + "github.com/perfect-panel/server/internal/model/log" "github.com/perfect-panel/server/pkg/constant" "github.com/perfect-panel/server/pkg/xerr" @@ -104,16 +105,24 @@ func (l *ResetTrafficLogic) ResetTraffic(req *types.ResetTrafficOrderRequest) (r return err } // create deduction record - deductionLog := user.GiftAmountLog{ - UserId: orderInfo.UserId, - OrderNo: orderInfo.OrderNo, - Amount: orderInfo.GiftAmount, - Type: 2, - Balance: u.GiftAmount, - Remark: "ResetTraffic order deduction", + giftLog := log.Gift{ + Type: log.GiftTypeReduce, + OrderNo: orderInfo.OrderNo, + SubscribeId: 0, + Amount: orderInfo.GiftAmount, + Balance: u.GiftAmount, + Remark: "Renewal order deduction", + Timestamp: time.Now().UnixMilli(), } - if err := db.Model(&user.GiftAmountLog{}).Create(&deductionLog).Error; err != nil { - l.Errorw("[ResetTraffic] Database insert error", logger.Field("error", err.Error()), logger.Field("deductionLog", deductionLog)) + content, _ := giftLog.Marshal() + + if err = db.Model(&log.SystemLog{}).Create(&log.SystemLog{ + Type: log.TypeGift.Uint8(), + Date: time.Now().Format(time.DateOnly), + ObjectID: u.Id, + Content: string(content), + }).Error; err != nil { + l.Errorw("[ResetTraffic] Database insert error", logger.Field("error", err.Error()), logger.Field("deductionLog", content)) return err } } diff --git a/internal/logic/public/portal/getSubscriptionLogic.go b/internal/logic/public/portal/getSubscriptionLogic.go index 8796f1d..564b591 100644 --- a/internal/logic/public/portal/getSubscriptionLogic.go +++ b/internal/logic/public/portal/getSubscriptionLogic.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" + "github.com/perfect-panel/server/internal/model/subscribe" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/logger" @@ -27,12 +28,18 @@ func NewGetSubscriptionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *G } } -func (l *GetSubscriptionLogic) GetSubscription() (resp *types.GetSubscriptionResponse, err error) { +func (l *GetSubscriptionLogic) GetSubscription(req *types.GetSubscriptionRequest) (resp *types.GetSubscriptionResponse, err error) { resp = &types.GetSubscriptionResponse{ List: make([]types.Subscribe, 0), } // Get the subscription list - data, err := l.svcCtx.SubscribeModel.QuerySubscribeListByShow(l.ctx) + _, data, err := l.svcCtx.SubscribeModel.FilterList(l.ctx, &subscribe.FilterParams{ + Page: 1, + Size: 9999, + Show: true, + Language: req.Language, + DefaultLanguage: true, + }) if err != nil { l.Errorw("[Site GetSubscription]", logger.Field("err", err.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get subscription list error: %v", err.Error()) diff --git a/internal/logic/public/portal/prePurchaseOrderLogic.go b/internal/logic/public/portal/prePurchaseOrderLogic.go index ac5ae6c..b2a99ba 100644 --- a/internal/logic/public/portal/prePurchaseOrderLogic.go +++ b/internal/logic/public/portal/prePurchaseOrderLogic.go @@ -54,7 +54,7 @@ func (l *PrePurchaseOrderLogic) PrePurchaseOrder(req *types.PrePurchaseOrderRequ } return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find coupon error: %v", err.Error()) } - if couponInfo.Count <= couponInfo.UsedCount { + if couponInfo.Count != 0 && couponInfo.Count <= couponInfo.UsedCount { return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponInsufficientUsage), "coupon used") } subs := tool.StringToInt64Slice(couponInfo.Subscribe) diff --git a/internal/logic/public/portal/purchaseCheckoutLogic.go b/internal/logic/public/portal/purchaseCheckoutLogic.go index 1b71b08..ff7faf2 100644 --- a/internal/logic/public/portal/purchaseCheckoutLogic.go +++ b/internal/logic/public/portal/purchaseCheckoutLogic.go @@ -4,7 +4,10 @@ import ( "context" "encoding/json" "strconv" + "time" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/report" "github.com/perfect-panel/server/pkg/constant" paymentPlatform "github.com/perfect-panel/server/pkg/payment" @@ -104,6 +107,17 @@ func (l *PurchaseCheckoutLogic) PurchaseCheckout(req *types.CheckoutOrderRequest CheckoutUrl: url, } + case paymentPlatform.CryptoSaaS: + // Process EPay payment - generates payment URL for redirect + url, err := l.CryptoSaaSPayment(paymentConfig, orderInfo, req.ReturnUrl) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "epayPayment error: %v", err.Error()) + } + resp = &types.CheckoutOrderResponse{ + CheckoutUrl: url, + Type: "url", // Client should redirect to URL + } + case paymentPlatform.Balance: // Process balance payment - validate user and process payment immediately if orderInfo.UserId == 0 { @@ -138,8 +152,8 @@ func (l *PurchaseCheckoutLogic) PurchaseCheckout(req *types.CheckoutOrderRequest // It handles currency conversion and creates a pre-payment trade for QR code scanning func (l *PurchaseCheckoutLogic) alipayF2fPayment(pay *payment.Payment, info *order.Order) (string, error) { // Parse Alipay F2F configuration from payment settings - f2FConfig := payment.AlipayF2FConfig{} - if err := json.Unmarshal([]byte(pay.Config), &f2FConfig); err != nil { + f2FConfig := &payment.AlipayF2FConfig{} + if err := f2FConfig.Unmarshal([]byte(pay.Config)); err != nil { l.Errorw("[PurchaseCheckout] Unmarshal Alipay config error", logger.Field("error", err.Error())) return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unmarshal error: %s", err.Error()) } @@ -189,8 +203,9 @@ func (l *PurchaseCheckoutLogic) alipayF2fPayment(pay *payment.Payment, info *ord // It supports various payment methods including WeChat Pay and Alipay through Stripe func (l *PurchaseCheckoutLogic) stripePayment(config string, info *order.Order, identifier string) (*types.StripePayment, error) { // Parse Stripe configuration from payment settings - stripeConfig := payment.StripeConfig{} - if err := json.Unmarshal([]byte(config), &stripeConfig); err != nil { + stripeConfig := &payment.StripeConfig{} + + if err := stripeConfig.Unmarshal([]byte(config)); err != nil { l.Errorw("[PurchaseCheckout] Unmarshal Stripe config error", logger.Field("error", err.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unmarshal error: %s", err.Error()) } @@ -247,14 +262,13 @@ func (l *PurchaseCheckoutLogic) stripePayment(config string, info *order.Order, // It handles currency conversion and creates a payment URL for external payment processing func (l *PurchaseCheckoutLogic) epayPayment(config *payment.Payment, info *order.Order, returnUrl string) (string, error) { // Parse EPay configuration from payment settings - epayConfig := payment.EPayConfig{} - if err := json.Unmarshal([]byte(config.Config), &epayConfig); err != nil { + epayConfig := &payment.EPayConfig{} + if err := epayConfig.Unmarshal([]byte(config.Config)); err != nil { l.Errorw("[PurchaseCheckout] Unmarshal EPay config error", logger.Field("error", err.Error())) return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unmarshal error: %s", err.Error()) } - // Initialize EPay client with merchant credentials - client := epay.NewClient(epayConfig.Pid, epayConfig.Url, epayConfig.Key) + client := epay.NewClient(epayConfig.Pid, epayConfig.Url, epayConfig.Key, epayConfig.Type) // Convert order amount to CNY using current exchange rate amount, err := l.queryExchangeRate("CNY", info.Amount) @@ -262,16 +276,29 @@ func (l *PurchaseCheckoutLogic) epayPayment(config *payment.Payment, info *order return "", err } + // gateway mod + + isGatewayMod := report.IsGatewayMode() + // Build notification URL for payment status callbacks notifyUrl := "" if config.Domain != "" { - notifyUrl = config.Domain + "/v1/notify/" + config.Platform + "/" + config.Token + notifyUrl = config.Domain + if isGatewayMod { + notifyUrl += "/api/" + } + notifyUrl = notifyUrl + "/v1/notify/" + config.Platform + "/" + config.Token } else { host, ok := l.ctx.Value(constant.CtxKeyRequestHost).(string) if !ok { host = l.svcCtx.Config.Host } - notifyUrl = "https://" + host + "/v1/notify/" + config.Platform + "/" + config.Token + + notifyUrl = "https://" + host + if isGatewayMod { + notifyUrl += "/api" + } + notifyUrl = notifyUrl + "/v1/notify/" + config.Platform + "/" + config.Token } // Create payment URL for user redirection @@ -286,12 +313,70 @@ func (l *PurchaseCheckoutLogic) epayPayment(config *payment.Payment, info *order return url, nil } +// CryptoSaaSPayment processes CryptoSaaSPayment payment by generating a payment URL for redirect +// It handles currency conversion and creates a payment URL for external payment processing +func (l *PurchaseCheckoutLogic) CryptoSaaSPayment(config *payment.Payment, info *order.Order, returnUrl string) (string, error) { + // Parse EPay configuration from payment settings + epayConfig := &payment.CryptoSaaSConfig{} + if err := epayConfig.Unmarshal([]byte(config.Config)); err != nil { + l.Errorw("[PurchaseCheckout] Unmarshal EPay config error", logger.Field("error", err.Error())) + return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unmarshal error: %s", err.Error()) + } + // Initialize EPay client with merchant credentials + client := epay.NewClient(epayConfig.AccountID, epayConfig.Endpoint, epayConfig.SecretKey, epayConfig.Type) + + // Convert order amount to CNY using current exchange rate + amount, err := l.queryExchangeRate("CNY", info.Amount) + if err != nil { + return "", err + } + + // gateway mod + isGatewayMod := report.IsGatewayMode() + + // Build notification URL for payment status callbacks + notifyUrl := "" + if config.Domain != "" { + notifyUrl = config.Domain + if isGatewayMod { + notifyUrl += "/api/" + } + notifyUrl = notifyUrl + "/v1/notify/" + config.Platform + "/" + config.Token + } else { + host, ok := l.ctx.Value(constant.CtxKeyRequestHost).(string) + if !ok { + host = l.svcCtx.Config.Host + } + + notifyUrl = "https://" + host + if isGatewayMod { + notifyUrl += "/api" + } + notifyUrl = notifyUrl + "/v1/notify/" + config.Platform + "/" + config.Token + } + // Create payment URL for user redirection + url := client.CreatePayUrl(epay.Order{ + Name: l.svcCtx.Config.Site.SiteName, + Amount: amount, + OrderNo: info.OrderNo, + SignType: "MD5", + NotifyUrl: notifyUrl, + ReturnUrl: returnUrl, + }) + return url, nil +} + // queryExchangeRate converts the order amount from system currency to target currency // It retrieves the current exchange rate and performs currency conversion if needed func (l *PurchaseCheckoutLogic) queryExchangeRate(to string, src int64) (amount float64, err error) { // Convert cents to decimal amount amount = float64(src) / float64(100) + if l.svcCtx.ExchangeRate != 0 && to == "CNY" { + amount = amount * l.svcCtx.ExchangeRate + return amount, nil + } + // Retrieve system currency configuration currency, err := l.svcCtx.SystemModel.GetCurrencyConfig(l.ctx) if err != nil { @@ -335,7 +420,7 @@ func (l *PurchaseCheckoutLogic) balancePayment(u *user.User, o *order.Order) err logger.Field("orderNo", o.OrderNo), logger.Field("userId", u.Id), ) - err := l.svcCtx.OrderModel.UpdateOrderStatus(l.ctx, o.OrderNo, 2) + 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()), @@ -386,16 +471,21 @@ func (l *PurchaseCheckoutLogic) balancePayment(u *user.User, o *order.Order) err // Create gift amount log if gift amount was used if giftUsed > 0 { - giftLog := &user.GiftAmountLog{ - UserId: u.Id, - UserSubscribeId: 0, // Will be updated when subscription is created - OrderNo: o.OrderNo, - Type: 2, // Type 2 represents gift amount decrease/usage - Amount: giftUsed, - Balance: userInfo.GiftAmount, - Remark: "Purchase payment", + giftLog := &log.Gift{ + OrderNo: o.OrderNo, + Type: log.GiftTypeReduce, // Type 2 represents gift amount decrease/usage + Amount: giftUsed, + Balance: userInfo.GiftAmount, + Remark: "Purchase payment", } - err = db.Create(giftLog).Error + content, _ := giftLog.Marshal() + + err = db.Create(&log.SystemLog{ + Type: log.TypeGift.Uint8(), + ObjectID: userInfo.Id, + Date: time.Now().Format(time.DateOnly), + Content: string(content), + }).Error if err != nil { return err } @@ -403,14 +493,20 @@ func (l *PurchaseCheckoutLogic) balancePayment(u *user.User, o *order.Order) err // Create balance log if regular balance was used if balanceUsed > 0 { - balanceLog := &user.BalanceLog{ - UserId: u.Id, - Amount: balanceUsed, - Type: 3, // Type 3 represents payment deduction - OrderId: o.Id, - Balance: userInfo.Balance, + balanceLog := &log.Balance{ + Amount: balanceUsed, + Type: log.BalanceTypePayment, // Type 3 represents payment deduction + OrderNo: o.OrderNo, + Balance: userInfo.Balance, + Timestamp: time.Now().UnixMilli(), } - err = db.Create(balanceLog).Error + content, _ := balanceLog.Marshal() + err = db.Create(&log.SystemLog{ + Type: log.TypeBalance.Uint8(), + ObjectID: userInfo.Id, + Date: time.Now().Format(time.DateOnly), + Content: string(content), + }).Error if err != nil { return err } @@ -418,7 +514,7 @@ func (l *PurchaseCheckoutLogic) balancePayment(u *user.User, o *order.Order) err // Store gift amount used in order for potential refund tracking o.GiftAmount = giftUsed - err = l.svcCtx.OrderModel.Update(l.ctx, o) + err = l.svcCtx.OrderModel.Update(l.ctx, o, db) if err != nil { return err } diff --git a/internal/logic/public/portal/purchaseLogic.go b/internal/logic/public/portal/purchaseLogic.go index 5bc8786..322f94c 100644 --- a/internal/logic/public/portal/purchaseLogic.go +++ b/internal/logic/public/portal/purchaseLogic.go @@ -6,18 +6,17 @@ import ( "fmt" "time" - "github.com/perfect-panel/server/pkg/payment" - - "github.com/perfect-panel/server/pkg/constant" - - "github.com/hibiken/asynq" "github.com/perfect-panel/server/internal/model/order" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/constant" "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/payment" "github.com/perfect-panel/server/pkg/tool" "github.com/perfect-panel/server/pkg/xerr" queue "github.com/perfect-panel/server/queue/types" + + "github.com/hibiken/asynq" "github.com/pkg/errors" "gorm.io/gorm" ) @@ -81,9 +80,15 @@ func (l *PurchaseLogic) Purchase(req *types.PortalPurchaseRequest) (resp *types. } return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find coupon error: %v", err.Error()) } - if couponInfo.Count <= couponInfo.UsedCount { + if couponInfo.Count != 0 && couponInfo.Count <= couponInfo.UsedCount { return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponInsufficientUsage), "coupon used") } + // Check expiration time + expireTime := time.Unix(couponInfo.ExpireTime, 0) + if time.Now().After(expireTime) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponExpired), "coupon expired") + } + couponSub := tool.StringToInt64Slice(couponInfo.Subscribe) if len(couponSub) > 0 && !tool.Contains(couponSub, req.SubscribeId) { return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponNotApplicable), "coupon not match") @@ -137,13 +142,15 @@ func (l *PurchaseLogic) Purchase(req *types.PortalPurchaseRequest) (resp *types. Password: req.Password, InviteCode: req.InviteCode, } - if _, err = l.svcCtx.Redis.Set(l.ctx, fmt.Sprintf(constant.TempOrderCacheKey, orderInfo.OrderNo), tempOrder.Marshal(), CloseOrderTimeMinutes*time.Minute).Result(); err != nil { + content, _ := tempOrder.Marshal() + + if _, err = l.svcCtx.Redis.Set(l.ctx, fmt.Sprintf(constant.TempOrderCacheKey, orderInfo.OrderNo), string(content), CloseOrderTimeMinutes*time.Minute).Result(); err != nil { l.Errorw("[Purchase] Redis set error", logger.Field("error", err.Error()), logger.Field("order_no", orderInfo.OrderNo)) return err } l.Infow("[Purchase] Guest order", logger.Field("order_no", orderInfo.OrderNo), logger.Field("identifier", req.Identifier)) // save guest order - if err := l.svcCtx.OrderModel.Insert(l.ctx, orderInfo, tx); err != nil { + if err = l.svcCtx.OrderModel.Insert(l.ctx, orderInfo, tx); err != nil { return err } return nil diff --git a/internal/logic/public/portal/queryPurchaseOrderLogic.go b/internal/logic/public/portal/queryPurchaseOrderLogic.go index bb1b8c3..d8e4795 100644 --- a/internal/logic/public/portal/queryPurchaseOrderLogic.go +++ b/internal/logic/public/portal/queryPurchaseOrderLogic.go @@ -95,7 +95,7 @@ func (l *QueryPurchaseOrderLogic) handleTemporaryOrder(orderInfo *order.Order, r } // Validate user and email - if err := l.validateUserAndEmail(orderInfo, req.Identifier, req.Identifier); err != nil { + if err = l.validateUserAndEmail(orderInfo, req.AuthType, req.Identifier); err != nil { return "", err } diff --git a/internal/logic/public/subscribe/queryApplicationConfigLogic.go b/internal/logic/public/subscribe/queryApplicationConfigLogic.go deleted file mode 100644 index fa939da..0000000 --- a/internal/logic/public/subscribe/queryApplicationConfigLogic.go +++ /dev/null @@ -1,116 +0,0 @@ -package subscribe - -import ( - "context" - - "github.com/perfect-panel/server/internal/model/application" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" - "gorm.io/gorm" - - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/internal/types" - "github.com/perfect-panel/server/pkg/logger" -) - -type QueryApplicationConfigLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Get application config -func NewQueryApplicationConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryApplicationConfigLogic { - return &QueryApplicationConfigLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *QueryApplicationConfigLogic) QueryApplicationConfig() (resp *types.ApplicationResponse, err error) { - resp = &types.ApplicationResponse{} - var applications []*application.Application - err = l.svcCtx.ApplicationModel.Transaction(l.ctx, func(tx *gorm.DB) (err error) { - return tx.Model(applications).Preload("ApplicationVersions").Find(&applications).Error - }) - if err != nil { - l.Errorw("[QueryApplicationConfig] get application error: ", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get application error: %v", err.Error()) - } - - if len(applications) == 0 { - return resp, nil - } - - for _, app := range applications { - applicationResponse := types.ApplicationResponseInfo{ - Id: app.Id, - Name: app.Name, - Icon: app.Icon, - Description: app.Description, - SubscribeType: app.SubscribeType, - } - applicationVersions := app.ApplicationVersions - if len(applicationVersions) != 0 { - for _, applicationVersion := range applicationVersions { - /*if !applicationVersion.IsDefault { - continue - }*/ - switch applicationVersion.Platform { - case "ios": - applicationResponse.Platform.IOS = append(applicationResponse.Platform.IOS, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - case "macos": - applicationResponse.Platform.MacOS = append(applicationResponse.Platform.MacOS, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - case "linux": - applicationResponse.Platform.Linux = append(applicationResponse.Platform.Linux, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - case "android": - applicationResponse.Platform.Android = append(applicationResponse.Platform.Android, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - case "windows": - applicationResponse.Platform.Windows = append(applicationResponse.Platform.Windows, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - case "harmony": - applicationResponse.Platform.Harmony = append(applicationResponse.Platform.Harmony, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - } - } - } - resp.Applications = append(resp.Applications, applicationResponse) - } - - return -} diff --git a/internal/logic/public/subscribe/querySubscribeListLogic.go b/internal/logic/public/subscribe/querySubscribeListLogic.go index 18299dd..0504e35 100644 --- a/internal/logic/public/subscribe/querySubscribeListLogic.go +++ b/internal/logic/public/subscribe/querySubscribeListLogic.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" + "github.com/perfect-panel/server/internal/model/subscribe" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/logger" @@ -27,15 +28,22 @@ func NewQuerySubscribeListLogic(ctx context.Context, svcCtx *svc.ServiceContext) } } -func (l *QuerySubscribeListLogic) QuerySubscribeList() (resp *types.QuerySubscribeListResponse, err error) { +func (l *QuerySubscribeListLogic) QuerySubscribeList(req *types.QuerySubscribeListRequest) (resp *types.QuerySubscribeListResponse, err error) { - data, err := l.svcCtx.SubscribeModel.QuerySubscribeList(l.ctx) + total, data, err := l.svcCtx.SubscribeModel.FilterList(l.ctx, &subscribe.FilterParams{ + Page: 1, + Size: 9999, + Language: req.Language, + Sell: true, + DefaultLanguage: true, + }) if err != nil { l.Errorw("[QuerySubscribeListLogic] Database Error", logger.Field("error", err.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "QuerySubscribeList error: %v", err.Error()) } + resp = &types.QuerySubscribeListResponse{ - Total: int64(len(data)), + Total: total, } list := make([]types.Subscribe, len(data)) for i, item := range data { diff --git a/internal/logic/public/subscribe/queryUserSubscribeNodeListLogic.go b/internal/logic/public/subscribe/queryUserSubscribeNodeListLogic.go new file mode 100644 index 0000000..2ad05b8 --- /dev/null +++ b/internal/logic/public/subscribe/queryUserSubscribeNodeListLogic.go @@ -0,0 +1,195 @@ +package subscribe + +import ( + "context" + "strings" + "time" + + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type QueryUserSubscribeNodeListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get user subscribe node info +func NewQueryUserSubscribeNodeListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryUserSubscribeNodeListLogic { + return &QueryUserSubscribeNodeListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryUserSubscribeNodeListLogic) QueryUserSubscribeNodeList() (resp *types.QueryUserSubscribeNodeListResponse, err error) { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + + userSubscribes, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, u.Id, 1, 2) + if err != nil { + logger.Errorw("failed to query user subscribe", logger.Field("error", err.Error()), logger.Field("user_id", u.Id)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "DB_ERROR") + } + + resp = &types.QueryUserSubscribeNodeListResponse{} + for _, us := range userSubscribes { + userSubscribe, err := l.getUserSubscribe(us.Token) + if err != nil { + l.Errorw("[SubscribeLogic] Get user subscribe failed", logger.Field("error", err.Error()), logger.Field("token", userSubscribe.Token)) + return nil, err + } + nodes, err := l.getServers(userSubscribe) + if err != nil { + return nil, err + } + userSubscribeInfo := types.UserSubscribeInfo{ + Id: userSubscribe.Id, + Nodes: nodes, + Traffic: userSubscribe.Traffic, + Upload: userSubscribe.Upload, + Download: userSubscribe.Download, + Token: userSubscribe.Token, + UserId: userSubscribe.UserId, + OrderId: userSubscribe.OrderId, + SubscribeId: userSubscribe.SubscribeId, + StartTime: userSubscribe.StartTime.Unix(), + ExpireTime: userSubscribe.ExpireTime.Unix(), + Status: userSubscribe.Status, + CreatedAt: userSubscribe.CreatedAt.Unix(), + UpdatedAt: userSubscribe.UpdatedAt.Unix(), + } + + if userSubscribe.FinishedAt != nil { + userSubscribeInfo.FinishedAt = userSubscribe.FinishedAt.Unix() + } + + if l.svcCtx.Config.Register.EnableTrial && l.svcCtx.Config.Register.TrialSubscribe == userSubscribe.SubscribeId { + userSubscribeInfo.IsTryOut = true + } + + resp.List = append(resp.List, userSubscribeInfo) + } + + return +} + +func (l *QueryUserSubscribeNodeListLogic) getServers(userSub *user.Subscribe) (userSubscribeNodes []*types.UserSubscribeNodeInfo, err error) { + userSubscribeNodes = make([]*types.UserSubscribeNodeInfo, 0) + if l.isSubscriptionExpired(userSub) { + return l.createExpiredServers(), nil + } + + subDetails, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, userSub.SubscribeId) + if err != nil { + l.Errorw("[Generate Subscribe]find subscribe details error: %v", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe details error: %v", err.Error()) + } + nodeIds := tool.StringToInt64Slice(subDetails.Nodes) + tags := strings.Split(subDetails.NodeTags, ",") + + l.Debugf("[Generate Subscribe]nodes: %v, NodeTags: %v", nodeIds, tags) + + enable := true + + _, nodes, err := l.svcCtx.NodeModel.FilterNodeList(l.ctx, &node.FilterNodeParams{ + Page: 0, + Size: 1000, + NodeId: nodeIds, + Enabled: &enable, // Only get enabled nodes + }) + + if len(nodes) > 0 { + var serverMapIds = make(map[int64]*node.Server) + for _, n := range nodes { + serverMapIds[n.ServerId] = nil + } + var serverIds []int64 + for k := range serverMapIds { + serverIds = append(serverIds, k) + } + + servers, err := l.svcCtx.NodeModel.QueryServerList(l.ctx, serverIds) + if err != nil { + l.Errorw("[Generate Subscribe]find server details error: %v", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find server details error: %v", err.Error()) + } + + for _, s := range servers { + serverMapIds[s.Id] = s + } + + for _, n := range nodes { + server := serverMapIds[n.ServerId] + if server == nil { + continue + } + userSubscribeNode := &types.UserSubscribeNodeInfo{ + Id: n.Id, + Name: n.Name, + Uuid: userSub.UUID, + Protocol: n.Protocol, + Port: n.Port, + Address: n.Address, + Tags: strings.Split(n.Tags, ","), + Country: server.Country, + City: server.City, + CreatedAt: n.CreatedAt.Unix(), + } + userSubscribeNodes = append(userSubscribeNodes, userSubscribeNode) + } + } + + l.Debugf("[Query Subscribe]found servers: %v", len(nodes)) + + if err != nil { + l.Errorw("[Generate Subscribe]find server details error: %v", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find server details error: %v", err.Error()) + } + logger.Debugf("[Generate Subscribe]found servers: %v", len(nodes)) + return userSubscribeNodes, nil +} + +func (l *QueryUserSubscribeNodeListLogic) isSubscriptionExpired(userSub *user.Subscribe) bool { + return userSub.ExpireTime.Unix() < time.Now().Unix() && userSub.ExpireTime.Unix() != 0 +} + +func (l *QueryUserSubscribeNodeListLogic) createExpiredServers() []*types.UserSubscribeNodeInfo { + return nil +} + +func (l *QueryUserSubscribeNodeListLogic) getFirstHostLine() string { + host := l.svcCtx.Config.Host + lines := strings.Split(host, "\n") + if len(lines) > 0 { + return lines[0] + } + return host +} +func (l *QueryUserSubscribeNodeListLogic) getUserSubscribe(token string) (*user.Subscribe, error) { + userSub, err := l.svcCtx.UserModel.FindOneSubscribeByToken(l.ctx, token) + if err != nil { + l.Infow("[Generate Subscribe]find subscribe error: %v", logger.Field("error", err.Error()), logger.Field("token", token)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe error: %v", err.Error()) + } + + // Ignore expiration check + //if userSub.Status > 1 { + // l.Infow("[Generate Subscribe]subscribe is not available", logger.Field("status", int(userSub.Status)), logger.Field("token", token)) + // return nil, errors.Wrapf(xerr.NewErrCode(xerr.SubscribeNotAvailable), "subscribe is not available") + //} + + return userSub, nil +} diff --git a/internal/logic/public/user/commissionWithdrawLogic.go b/internal/logic/public/user/commissionWithdrawLogic.go new file mode 100644 index 0000000..d16dec0 --- /dev/null +++ b/internal/logic/public/user/commissionWithdrawLogic.go @@ -0,0 +1,108 @@ +package user + +import ( + "context" + "time" + + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type CommissionWithdrawLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Commission Withdraw +func NewCommissionWithdrawLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CommissionWithdrawLogic { + return &CommissionWithdrawLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CommissionWithdrawLogic) CommissionWithdraw(req *types.CommissionWithdrawRequest) (resp *types.WithdrawalLog, err error) { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + + if u.Commission < req.Amount { + logger.Errorf("User %d has insufficient commission balance: %.2f, requested: %.2f", u.Id, float64(u.Commission)/100, float64(req.Amount)/100) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserCommissionNotEnough), "User %d has insufficient commission balance", u.Id) + } + + tx := l.svcCtx.DB.WithContext(l.ctx).Begin() + + // update user commission balance + u.Commission -= req.Amount + if err = l.svcCtx.UserModel.Update(l.ctx, u, tx); err != nil { + tx.Rollback() + l.Errorf("Failed to update user %d commission balance: %v", u.Id, err) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "Failed to update user %d commission balance: %v", u.Id, err) + } + + // create withdrawal log + logInfo := log.Commission{ + Type: log.CommissionTypeConvertBalance, + Amount: req.Amount, + Timestamp: time.Now().UnixMilli(), + } + b, err := logInfo.Marshal() + + if err != nil { + tx.Rollback() + l.Errorf("Failed to marshal commission log for user %d: %v", u.Id, err) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Failed to marshal commission log for user %d: %v", u.Id, err) + } + + err = tx.Model(log.SystemLog{}).Create(&log.SystemLog{ + Type: log.TypeCommission.Uint8(), + Date: time.Now().Format("2006-01-02"), + ObjectID: u.Id, + Content: string(b), + CreatedAt: time.Now(), + }).Error + + if err != nil { + tx.Rollback() + l.Errorf("Failed to create commission log for user %d: %v", u.Id, err) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "Failed to create commission log for user %d: %v", u.Id, err) + } + + err = tx.Model(&user.Withdrawal{}).Create(&user.Withdrawal{ + UserId: u.Id, + Amount: req.Amount, + Content: req.Content, + Status: 0, + Reason: "", + }).Error + + if err != nil { + tx.Rollback() + l.Errorf("Failed to create withdrawal log for user %d: %v", u.Id, err) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "Failed to create withdrawal log for user %d: %v", u.Id, err) + } + if err = tx.Commit().Error; err != nil { + l.Errorf("Transaction commit failed for user %d withdrawal: %v", u.Id, err) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Transaction commit failed for user %d withdrawal: %v", u.Id, err) + } + + return &types.WithdrawalLog{ + UserId: u.Id, + Amount: req.Amount, + Content: req.Content, + Status: 0, + Reason: "", + CreatedAt: time.Now().UnixMilli(), + }, nil +} diff --git a/internal/logic/public/user/getDeviceListLogic.go b/internal/logic/public/user/getDeviceListLogic.go new file mode 100644 index 0000000..76722d5 --- /dev/null +++ b/internal/logic/public/user/getDeviceListLogic.go @@ -0,0 +1,39 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" +) + +type GetDeviceListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get Device List +func NewGetDeviceListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetDeviceListLogic { + return &GetDeviceListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetDeviceListLogic) GetDeviceList() (resp *types.GetDeviceListResponse, err error) { + userInfo := l.ctx.Value(constant.CtxKeyUser).(*user.User) + list, count, err := l.svcCtx.UserModel.QueryDeviceList(l.ctx, userInfo.Id) + userRespList := make([]types.UserDevice, 0) + tool.DeepCopy(&userRespList, list) + resp = &types.GetDeviceListResponse{ + Total: count, + List: userRespList, + } + return +} diff --git a/internal/logic/public/user/getLoginLogLogic.go b/internal/logic/public/user/getLoginLogLogic.go index ce61fa8..a6637f4 100644 --- a/internal/logic/public/user/getLoginLogLogic.go +++ b/internal/logic/public/user/getLoginLogLogic.go @@ -3,13 +3,13 @@ package user import ( "context" + "github.com/perfect-panel/server/internal/model/log" "github.com/perfect-panel/server/pkg/constant" "github.com/perfect-panel/server/internal/model/user" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/tool" "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -35,15 +35,34 @@ func (l *GetLoginLogLogic) GetLoginLog(req *types.GetLoginLogRequest) (resp *typ logger.Error("current user is not found in context") return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") } - data, total, err := l.svcCtx.UserModel.FilterLoginLogList(l.ctx, req.Page, req.Size, &user.LoginLogFilterParams{ - UserId: u.Id, + data, total, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{ + Page: req.Page, + Size: req.Size, + Type: log.TypeLogin.Uint8(), + ObjectID: u.Id, }) if err != nil { l.Errorw("find login log failed:", logger.Field("error", err.Error()), logger.Field("user_id", u.Id)) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find login log failed: %v", err.Error()) } list := make([]types.UserLoginLog, 0) - tool.DeepCopy(&list, data) + + for _, datum := range data { + var content log.Login + if err = content.Unmarshal([]byte(datum.Content)); err != nil { + l.Errorf("[GetUserLoginLogs] unmarshal login log content failed: %v", err.Error()) + continue + } + list = append(list, types.UserLoginLog{ + Id: datum.Id, + UserId: datum.ObjectID, + LoginIP: content.LoginIP, + UserAgent: content.UserAgent, + Success: content.Success, + Timestamp: datum.CreatedAt.UnixMilli(), + }) + } + return &types.GetLoginLogResponse{ Total: total, List: list, diff --git a/internal/logic/public/user/getSubscribeLogLogic.go b/internal/logic/public/user/getSubscribeLogLogic.go index a24308b..eeb51b9 100644 --- a/internal/logic/public/user/getSubscribeLogLogic.go +++ b/internal/logic/public/user/getSubscribeLogLogic.go @@ -3,13 +3,13 @@ package user import ( "context" + "github.com/perfect-panel/server/internal/model/log" "github.com/perfect-panel/server/pkg/constant" "github.com/perfect-panel/server/internal/model/user" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/tool" "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -20,7 +20,7 @@ type GetSubscribeLogLogic struct { svcCtx *svc.ServiceContext } -// Get Subscribe Log +// NewGetSubscribeLogLogic Get Subscribe Log func NewGetSubscribeLogLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetSubscribeLogLogic { return &GetSubscribeLogLogic{ Logger: logger.WithContext(ctx), @@ -35,15 +35,34 @@ func (l *GetSubscribeLogLogic) GetSubscribeLog(req *types.GetSubscribeLogRequest logger.Error("current user is not found in context") return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") } - data, total, err := l.svcCtx.UserModel.FilterSubscribeLogList(l.ctx, req.Page, req.Size, &user.SubscribeLogFilterParams{ - UserId: u.Id, + data, total, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{ + Page: req.Page, + Size: req.Size, + Type: log.TypeSubscribe.Uint8(), + ObjectID: u.Id, // filter by current user id }) if err != nil { l.Errorw("[GetUserSubscribeLogs] Get User Subscribe Logs Error:", logger.Field("err", err.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Get User Subscribe Logs Error") } var list []types.UserSubscribeLog - tool.DeepCopy(&list, data) + + for _, item := range data { + var content log.Subscribe + if err = content.Unmarshal([]byte(item.Content)); err != nil { + l.Errorf("[GetUserSubscribeLogs] unmarshal subscribe log content failed: %v", err.Error()) + continue + } + list = append(list, types.UserSubscribeLog{ + Id: item.Id, + UserId: item.ObjectID, + UserSubscribeId: content.UserSubscribeId, + Token: content.Token, + IP: content.ClientIP, + UserAgent: content.UserAgent, + Timestamp: item.CreatedAt.UnixMilli(), + }) + } return &types.GetSubscribeLogResponse{ List: list, diff --git a/internal/logic/public/user/queryUserAffiliateLogic.go b/internal/logic/public/user/queryUserAffiliateLogic.go index e0435da..7c8e731 100644 --- a/internal/logic/public/user/queryUserAffiliateLogic.go +++ b/internal/logic/public/user/queryUserAffiliateLogic.go @@ -3,6 +3,7 @@ package user import ( "context" + "github.com/perfect-panel/server/internal/model/log" "github.com/perfect-panel/server/pkg/constant" "github.com/perfect-panel/server/internal/model/user" @@ -44,14 +45,20 @@ func (l *QueryUserAffiliateLogic) QueryUserAffiliate() (resp *types.QueryUserAff if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query User Affiliate failed: %v", err) } - err = l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { - return db.Model(&user.CommissionLog{}). - Where("user_id = ?", u.Id). - Select("COALESCE(SUM(amount), 0)"). - Scan(&sum).Error + data, _, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{ + Page: 1, + Size: 99999, + Type: log.TypeCommission.Uint8(), + ObjectID: u.Id, }) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query User Affiliate failed: %v", err) + + for _, datum := range data { + content := log.Commission{} + if err = content.Unmarshal([]byte(datum.Content)); err != nil { + l.Errorf("[QueryUserAffiliate] unmarshal comission log failed: %v", err.Error()) + continue + } + sum += content.Amount } return &types.QueryUserAffiliateCountResponse{ diff --git a/internal/logic/public/user/queryUserBalanceLogLogic.go b/internal/logic/public/user/queryUserBalanceLogLogic.go index 2a3c72d..e8c6d8f 100644 --- a/internal/logic/public/user/queryUserBalanceLogLogic.go +++ b/internal/logic/public/user/queryUserBalanceLogLogic.go @@ -3,17 +3,15 @@ package user import ( "context" + "github.com/perfect-panel/server/internal/model/log" "github.com/perfect-panel/server/pkg/constant" "github.com/perfect-panel/server/internal/model/user" - "github.com/perfect-panel/server/pkg/tool" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" - "gorm.io/gorm" - "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" ) type QueryUserBalanceLogLogic struct { @@ -22,7 +20,7 @@ type QueryUserBalanceLogLogic struct { svcCtx *svc.ServiceContext } -// Query User Balance Log +// NewQueryUserBalanceLogLogic Query User Balance Log func NewQueryUserBalanceLogLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryUserBalanceLogLogic { return &QueryUserBalanceLogLogic{ Logger: logger.WithContext(ctx), @@ -37,19 +35,37 @@ func (l *QueryUserBalanceLogLogic) QueryUserBalanceLog() (resp *types.QueryUserB logger.Error("current user is not found in context") return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") } - var data []*user.BalanceLog - var total int64 - // Query User Balance Log - err = l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { - return db.Model(&user.BalanceLog{}).Order("created_at DESC").Where("user_id = ?", u.Id).Count(&total).Find(&data).Error + + data, total, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{ + Page: 1, + Size: 99999, + Type: log.TypeBalance.Uint8(), + ObjectID: u.Id, }) if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query User Balance Log failed: %v", err) + l.Errorw("[QueryUserBalanceLog] Query User Balance Log Error:", logger.Field("err", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query User Balance Log Error") } - resp = &types.QueryUserBalanceLogListResponse{ - List: make([]types.UserBalanceLog, 0), + + list := make([]types.BalanceLog, 0) + for _, datum := range data { + var content log.Balance + if err = content.Unmarshal([]byte(datum.Content)); err != nil { + l.Errorf("[QueryUserBalanceLog] unmarshal balance log content failed: %v", err.Error()) + continue + } + list = append(list, types.BalanceLog{ + UserId: datum.ObjectID, + Amount: content.Amount, + Type: content.Type, + OrderNo: content.OrderNo, + Balance: content.Balance, + Timestamp: content.Timestamp, + }) + } + + return &types.QueryUserBalanceLogListResponse{ Total: total, - } - tool.DeepCopy(&resp.List, data) - return + List: list, + }, nil } diff --git a/internal/logic/public/user/queryUserCommissionLogLogic.go b/internal/logic/public/user/queryUserCommissionLogLogic.go index 8038197..c005828 100644 --- a/internal/logic/public/user/queryUserCommissionLogLogic.go +++ b/internal/logic/public/user/queryUserCommissionLogLogic.go @@ -3,17 +3,15 @@ package user import ( "context" + "github.com/perfect-panel/server/internal/model/log" "github.com/perfect-panel/server/pkg/constant" "github.com/perfect-panel/server/internal/model/user" - "github.com/perfect-panel/server/pkg/tool" - "github.com/perfect-panel/server/pkg/xerr" - "github.com/pkg/errors" - "gorm.io/gorm" - "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" ) type QueryUserCommissionLogLogic struct { @@ -32,22 +30,40 @@ func NewQueryUserCommissionLogLogic(ctx context.Context, svcCtx *svc.ServiceCont } func (l *QueryUserCommissionLogLogic) QueryUserCommissionLog(req *types.QueryUserCommissionLogListRequest) (resp *types.QueryUserCommissionLogListResponse, err error) { - var data []*user.CommissionLog u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) if !ok { logger.Error("current user is not found in context") return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") } - err = l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { - return db.Order("id desc").Limit(req.Size).Offset((req.Page-1)*req.Size).Where("user_id = ?", u.Id).Find(&data).Error + data, total, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{ + Page: req.Page, + Size: req.Size, + Type: log.TypeCommission.Uint8(), + ObjectID: u.Id, }) if err != nil { l.Errorw("Query User Commission Log failed", logger.Field("error", err.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query User Commission Log failed: %v", err) } var list []types.CommissionLog - tool.DeepCopy(&list, data) + + for _, datum := range data { + var content log.Commission + if err = content.Unmarshal([]byte(datum.Content)); err != nil { + l.Errorf("unmarshal commission log content failed: %v", err.Error()) + continue + } + list = append(list, types.CommissionLog{ + UserId: datum.ObjectID, + Type: content.Type, + Amount: content.Amount, + OrderNo: content.OrderNo, + Timestamp: content.Timestamp, + }) + } + return &types.QueryUserCommissionLogListResponse{ - List: list, + List: list, + Total: total, }, nil } diff --git a/internal/logic/public/user/queryUserInfoLogic.go b/internal/logic/public/user/queryUserInfoLogic.go index 3753ef9..cf51020 100644 --- a/internal/logic/public/user/queryUserInfoLogic.go +++ b/internal/logic/public/user/queryUserInfoLogic.go @@ -2,6 +2,7 @@ package user import ( "context" + "sort" "github.com/perfect-panel/server/pkg/constant" "github.com/perfect-panel/server/pkg/xerr" @@ -53,10 +54,31 @@ func (l *QueryUserInfoLogic) QueryUserInfo() (resp *types.User, err error) { } userMethods = append(userMethods, item) } + + // 按照指定顺序排序:email第一位,mobile第二位,其他按原顺序 + sort.Slice(userMethods, func(i, j int) bool { + return getAuthTypePriority(userMethods[i].AuthType) < getAuthTypePriority(userMethods[j].AuthType) + }) + resp.AuthMethods = userMethods return resp, nil } +// getAuthTypePriority 获取认证类型的排序优先级 +// email: 1 (第一位) +// mobile: 2 (第二位) +// 其他类型: 100+ (后续位置) +func getAuthTypePriority(authType string) int { + switch authType { + case "email": + return 1 + case "mobile": + return 2 + default: + return 100 + } +} + // maskOpenID 脱敏 OpenID,只保留前 3 和后 3 位 func maskOpenID(openID string) string { length := len(openID) diff --git a/internal/logic/public/user/queryUserSubscribeLogic.go b/internal/logic/public/user/queryUserSubscribeLogic.go index f570a48..55e3770 100644 --- a/internal/logic/public/user/queryUserSubscribeLogic.go +++ b/internal/logic/public/user/queryUserSubscribeLogic.go @@ -60,25 +60,8 @@ 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 - } - + short, _ := tool.FixedUniqueString(item.Token, 8, "") + sub.Short = short sub.ResetTime = calculateNextResetTime(&sub) resp.List = append(resp.List, sub) } @@ -87,7 +70,7 @@ func (l *QueryUserSubscribeLogic) QueryUserSubscribe() (resp *types.QueryUserSub // 计算下次重置时间 func calculateNextResetTime(sub *types.UserSubscribe) int64 { - startTime := time.UnixMilli(sub.StartTime) + resetTime := time.UnixMilli(sub.ExpireTime) now := time.Now() switch sub.Subscribe.ResetCycle { case 0: @@ -95,15 +78,15 @@ func calculateNextResetTime(sub *types.UserSubscribe) int64 { case 1: return time.Date(now.Year(), now.Month()+1, 1, 0, 0, 0, 0, now.Location()).UnixMilli() case 2: - if startTime.Day() > now.Day() { - return time.Date(now.Year(), now.Month(), startTime.Day(), 0, 0, 0, 0, now.Location()).UnixMilli() + if resetTime.Day() > now.Day() { + return time.Date(now.Year(), now.Month(), resetTime.Day(), 0, 0, 0, 0, now.Location()).UnixMilli() } else { - return time.Date(now.Year(), now.Month()+1, startTime.Day(), 0, 0, 0, 0, now.Location()).UnixMilli() + return time.Date(now.Year(), now.Month()+1, resetTime.Day(), 0, 0, 0, 0, now.Location()).UnixMilli() } case 3: - targetTime := time.Date(now.Year(), startTime.Month(), startTime.Day(), 0, 0, 0, 0, now.Location()) + targetTime := time.Date(now.Year(), resetTime.Month(), resetTime.Day(), 0, 0, 0, 0, now.Location()) if targetTime.Before(now) { - targetTime = time.Date(now.Year()+1, startTime.Month(), startTime.Day(), 0, 0, 0, 0, now.Location()) + targetTime = time.Date(now.Year()+1, resetTime.Month(), resetTime.Day(), 0, 0, 0, 0, now.Location()) } return targetTime.UnixMilli() default: diff --git a/internal/logic/public/user/queryWithdrawalLogLogic.go b/internal/logic/public/user/queryWithdrawalLogLogic.go new file mode 100644 index 0000000..1b1a583 --- /dev/null +++ b/internal/logic/public/user/queryWithdrawalLogLogic.go @@ -0,0 +1,30 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" +) + +type QueryWithdrawalLogLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewQueryWithdrawalLogLogic Query Withdrawal Log +func NewQueryWithdrawalLogLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryWithdrawalLogLogic { + return &QueryWithdrawalLogLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryWithdrawalLogLogic) QueryWithdrawalLog(req *types.QueryWithdrawalLogListRequest) (resp *types.QueryWithdrawalLogListResponse, err error) { + // todo: add your logic here and delete this line + + return +} diff --git a/internal/logic/public/user/resetUserSubscribeTokenLogic.go b/internal/logic/public/user/resetUserSubscribeTokenLogic.go index edf9342..febcae7 100644 --- a/internal/logic/public/user/resetUserSubscribeTokenLogic.go +++ b/internal/logic/public/user/resetUserSubscribeTokenLogic.go @@ -2,9 +2,10 @@ package user import ( "context" - "github.com/perfect-panel/server/internal/model/order" "time" + "github.com/perfect-panel/server/internal/model/order" + "github.com/perfect-panel/server/pkg/constant" "github.com/google/uuid" @@ -72,5 +73,16 @@ func (l *ResetUserSubscribeTokenLogic) ResetUserSubscribeToken(req *types.ResetU l.Errorw("UpdateSubscribe failed:", logger.Field("error", err.Error())) return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "UpdateSubscribe failed: %v", err.Error()) } + //clear user subscription cache + if err = l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, &newSub); err != nil { + l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("userSubscribeId", userSub.Id)) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "ClearSubscribeCache failed: %v", err.Error()) + } + // Clear subscription cache + if err = l.svcCtx.SubscribeModel.ClearCache(l.ctx, userSub.SubscribeId); err != nil { + l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("subscribeId", userSub.SubscribeId)) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "ClearSubscribeCache failed: %v", err.Error()) + } + return nil } diff --git a/internal/logic/public/user/unbindDeviceLogic.go b/internal/logic/public/user/unbindDeviceLogic.go new file mode 100644 index 0000000..57218cc --- /dev/null +++ b/internal/logic/public/user/unbindDeviceLogic.go @@ -0,0 +1,72 @@ +package user + +import ( + "context" + "fmt" + + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type UnbindDeviceLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Unbind Device +func NewUnbindDeviceLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UnbindDeviceLogic { + return &UnbindDeviceLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UnbindDeviceLogic) UnbindDevice(req *types.UnbindDeviceRequest) error { + userInfo := l.ctx.Value(constant.CtxKeyUser).(*user.User) + device, err := l.svcCtx.UserModel.FindOneDevice(l.ctx, req.Id) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DeviceNotExist), "find device") + } + + if device.UserId != userInfo.Id { + return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "device not belong to user") + } + + return l.svcCtx.DB.Transaction(func(tx *gorm.DB) error { + var deleteDevice user.Device + err = tx.Model(&deleteDevice).Where("id = ?", req.Id).First(&deleteDevice).Error + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.QueueEnqueueError), "find device err: %v", err) + } + err = tx.Delete(deleteDevice).Error + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete device err: %v", err) + } + var userAuth user.AuthMethods + err = tx.Model(&userAuth).Where("auth_identifier = ? and auth_type = ?", deleteDevice.Identifier, "device").First(&userAuth).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find device online record err: %v", err) + } + + err = tx.Delete(&userAuth).Error + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete device online record err: %v", err) + } + sessionId := l.ctx.Value(constant.CtxKeySessionID) + sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId) + l.svcCtx.Redis.Del(l.ctx, sessionIdCacheKey) + return nil + }) +} diff --git a/internal/logic/public/user/unbindOAuthLogic.go b/internal/logic/public/user/unbindOAuthLogic.go index 0520762..efe0ba1 100644 --- a/internal/logic/public/user/unbindOAuthLogic.go +++ b/internal/logic/public/user/unbindOAuthLogic.go @@ -42,6 +42,7 @@ func (l *UnbindOAuthLogic) UnbindOAuth(req *types.UnbindOAuthRequest) error { l.Errorw("delete user auth methods failed:", logger.Field("error", err.Error())) return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete user auth methods failed: %v", err.Error()) } + return nil } func (l *UnbindOAuthLogic) validator(req *types.UnbindOAuthRequest) bool { diff --git a/internal/logic/public/user/unsubscribeLogic.go b/internal/logic/public/user/unsubscribeLogic.go index e2c38b1..d3390fe 100644 --- a/internal/logic/public/user/unsubscribeLogic.go +++ b/internal/logic/public/user/unsubscribeLogic.go @@ -2,8 +2,11 @@ package user import ( "context" + "time" + "github.com/perfect-panel/server/internal/model/log" "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/tool" "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" @@ -38,6 +41,22 @@ func (l *UnsubscribeLogic) Unsubscribe(req *types.UnsubscribeRequest) error { logger.Error("current user is not found in context") return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") } + + // find user subscription by ID + userSub, err := l.svcCtx.UserModel.FindOneSubscribe(l.ctx, req.Id) + if err != nil { + l.Errorw("FindOneSubscribe failed", logger.Field("error", err.Error()), logger.Field("reqId", req.Id)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindOneSubscribe failed: %v", err.Error()) + } + + activate := []uint8{0, 1, 2} + + if !tool.Contains(activate, userSub.Status) { + // Only active (2) or paused (5) subscriptions can be cancelled + l.Errorw("Subscription status invalid for cancellation", logger.Field("userSubscribeId", userSub.Id), logger.Field("status", userSub.Status)) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Subscription status invalid for cancellation") + } + // Calculate the remaining amount to refund based on unused subscription time/traffic remainingAmount, err := CalculateRemainingAmount(l.ctx, l.svcCtx, req.Id) if err != nil { @@ -47,12 +66,8 @@ func (l *UnsubscribeLogic) Unsubscribe(req *types.UnsubscribeRequest) error { // Process unsubscription in a database transaction to ensure data consistency err = l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { // Find and update subscription status to cancelled (status = 4) - var userSub user.Subscribe - if err = db.Model(&user.Subscribe{}).Where("id = ?", req.Id).First(&userSub).Error; err != nil { - return err - } userSub.Status = 4 // Set status to cancelled - if err = l.svcCtx.UserModel.UpdateSubscribe(l.ctx, &userSub); err != nil { + if err = l.svcCtx.UserModel.UpdateSubscribe(l.ctx, userSub); err != nil { return err } @@ -83,30 +98,44 @@ func (l *UnsubscribeLogic) Unsubscribe(req *types.UnsubscribeRequest) error { // Create balance log entry only if there's an actual regular balance refund balanceRefundAmount := balance - u.Balance if balanceRefundAmount > 0 { - balanceLog := user.BalanceLog{ - UserId: userSub.UserId, - OrderId: userSub.OrderId, - Amount: balanceRefundAmount, - Type: 4, // Type 4 represents refund transaction - Balance: balance, + balanceLog := log.Balance{ + OrderNo: orderInfo.OrderNo, + Amount: balanceRefundAmount, + Type: log.BalanceTypeRefund, // Type 4 represents refund transaction + Balance: balance, + Timestamp: time.Now().UnixMilli(), } - if err := db.Model(&user.BalanceLog{}).Create(&balanceLog).Error; err != nil { + content, _ := balanceLog.Marshal() + + if err := db.Model(&log.SystemLog{}).Create(&log.SystemLog{ + Type: log.TypeBalance.Uint8(), + Date: time.Now().Format(time.DateOnly), + ObjectID: u.Id, + Content: string(content), + }).Error; err != nil { return err } } // Create gift amount log entry if there's a gift balance refund if gift > 0 { - giftLog := user.GiftAmountLog{ - UserId: userSub.UserId, - UserSubscribeId: userSub.Id, - OrderNo: orderInfo.OrderNo, - Type: 1, // Type 1 represents gift amount increase - Amount: gift, - Balance: u.GiftAmount + gift, - Remark: "Unsubscribe refund", + + giftLog := log.Gift{ + SubscribeId: userSub.Id, + OrderNo: orderInfo.OrderNo, + Type: log.GiftTypeIncrease, // Type 1 represents gift amount increase + Amount: gift, + Balance: u.GiftAmount + gift, + Remark: "Unsubscribe refund", } - if err := db.Model(&user.GiftAmountLog{}).Create(&giftLog).Error; err != nil { + content, _ := giftLog.Marshal() + + if err := db.Model(&log.SystemLog{}).Create(&log.SystemLog{ + Type: log.TypeGift.Uint8(), + Date: time.Now().Format(time.DateOnly), + ObjectID: u.Id, + Content: string(content), + }).Error; err != nil { return err } // Update user's gift amount @@ -118,5 +147,21 @@ func (l *UnsubscribeLogic) Unsubscribe(req *types.UnsubscribeRequest) error { return l.svcCtx.UserModel.Update(l.ctx, u) }) + if err != nil { + l.Errorw("Unsubscribe transaction failed", logger.Field("error", err.Error()), logger.Field("userId", u.Id), logger.Field("reqId", req.Id)) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unsubscribe transaction failed: %v", err.Error()) + } + + //clear user subscription cache + if err = l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, userSub); err != nil { + l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("userSubscribeId", userSub.Id)) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "ClearSubscribeCache failed: %v", err.Error()) + } + // Clear subscription cache + if err = l.svcCtx.SubscribeModel.ClearCache(l.ctx, userSub.SubscribeId); err != nil { + l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("subscribeId", userSub.SubscribeId)) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "ClearSubscribeCache failed: %v", err.Error()) + } + return err } diff --git a/internal/logic/public/user/updateUserRulesLogic.go b/internal/logic/public/user/updateUserRulesLogic.go new file mode 100644 index 0000000..63ab169 --- /dev/null +++ b/internal/logic/public/user/updateUserRulesLogic.go @@ -0,0 +1,51 @@ +package user + +import ( + "context" + "encoding/json" + + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type UpdateUserRulesLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewUpdateUserRulesLogic Update User Rules +func NewUpdateUserRulesLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateUserRulesLogic { + return &UpdateUserRulesLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateUserRulesLogic) UpdateUserRules(req *types.UpdateUserRulesRequest) error { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + if len(req.Rules) > 0 { + bytes, err := json.Marshal(req.Rules) + if err != nil { + l.Logger.Errorf("UpdateUserRulesLogic json marshal rules error: %v", err) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "json marshal rules failed: %v", err.Error()) + } + u.Rules = string(bytes) + err = l.svcCtx.UserModel.Update(l.ctx, u) + if err != nil { + l.Logger.Errorf("UpdateUserRulesLogic UpdateUserRules error: %v", err) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update user rules failed: %v", err.Error()) + } + } + return nil +} diff --git a/internal/logic/public/user/updateUserSubscribeNoteLogic.go b/internal/logic/public/user/updateUserSubscribeNoteLogic.go new file mode 100644 index 0000000..3c43a8d --- /dev/null +++ b/internal/logic/public/user/updateUserSubscribeNoteLogic.go @@ -0,0 +1,73 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/server/pkg/constant" + + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type UpdateUserSubscribeNoteLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewUpdateUserSubscribeNoteLogic Update User Subscribe Note +func NewUpdateUserSubscribeNoteLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateUserSubscribeNoteLogic { + return &UpdateUserSubscribeNoteLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateUserSubscribeNoteLogic) UpdateUserSubscribeNote(req *types.UpdateUserSubscribeNoteRequest) error { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + + userSub, err := l.svcCtx.UserModel.FindOneUserSubscribe(l.ctx, req.UserSubscribeId) + if err != nil { + l.Errorw("FindOneUserSubscribe failed:", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindOneUserSubscribe failed: %v", err.Error()) + } + + if userSub.UserId != u.Id { + l.Errorw("UserSubscribeId does not belong to the current user") + return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "UserSubscribeId does not belong to the current user") + } + + userSub.Note = req.Note + var newSub user.Subscribe + tool.DeepCopy(&newSub, userSub) + + err = l.svcCtx.UserModel.UpdateSubscribe(l.ctx, &newSub) + if err != nil { + l.Errorw("UpdateSubscribe failed:", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "UpdateSubscribe failed: %v", err.Error()) + } + + // Clear user subscription cache + if err = l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, &newSub); err != nil { + l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("userSubscribeId", userSub.Id)) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "ClearSubscribeCache failed: %v", err.Error()) + } + + // Clear subscription cache + if err = l.svcCtx.SubscribeModel.ClearCache(l.ctx, userSub.SubscribeId); err != nil { + l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("subscribeId", userSub.SubscribeId)) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "ClearSubscribeCache failed: %v", err.Error()) + } + + return nil +} diff --git a/internal/logic/server/constant.go b/internal/logic/server/constant.go index 0c54202..e2d1584 100644 --- a/internal/logic/server/constant.go +++ b/internal/logic/server/constant.go @@ -1,3 +1,86 @@ package server -const Unchanged = "Unchanged" +const ( + Unchanged = "Unchanged" + ShadowSocks = "shadowsocks" + Vmess = "vmess" + Vless = "vless" + Trojan = "trojan" + AnyTLS = "anytls" + Tuic = "tuic" + Hysteria = "hysteria" + // Deprecated: Hysteria2 is deprecated, use Hysteria instead + // TODO: remove in future versions + Hysteria2 = "hysteria2" +) + +type SecurityConfig struct { + SNI string `json:"sni"` + AllowInsecure *bool `json:"allow_insecure"` + Fingerprint string `json:"fingerprint"` + RealityServerAddress string `json:"reality_server_addr"` + RealityServerPort int `json:"reality_server_port"` + RealityPrivateKey string `json:"reality_private_key"` + RealityPublicKey string `json:"reality_public_key"` + RealityShortId string `json:"reality_short_id"` + RealityMldsa65seed string `json:"reality_mldsa65seed"` +} + +type TransportConfig struct { + Path string `json:"path"` + Host string `json:"host"` + ServiceName string `json:"service_name"` + DisableSNI bool `json:"disable_sni"` + ReduceRtt bool `json:"reduce_rtt"` + UDPRelayMode string `json:"udp_relay_mode"` + CongestionController string `json:"congestion_controller"` +} + +type VlessNode struct { + Port uint16 `json:"port"` + Flow string `json:"flow"` + Network string `json:"transport"` + TransportConfig *TransportConfig `json:"transport_config"` + Security string `json:"security"` + SecurityConfig *SecurityConfig `json:"security_config"` +} + +type VmessNode struct { + Port uint16 `json:"port"` + Network string `json:"transport"` + TransportConfig *TransportConfig `json:"transport_config"` + Security string `json:"security"` + SecurityConfig *SecurityConfig `json:"security_config"` +} + +type ShadowsocksNode struct { + Port uint16 `json:"port"` + Cipher string `json:"method"` + ServerKey string `json:"server_key"` +} + +type TrojanNode struct { + Port uint16 `json:"port"` + Network string `json:"transport"` + TransportConfig *TransportConfig `json:"transport_config"` + Security string `json:"security"` + SecurityConfig *SecurityConfig `json:"security_config"` +} + +type AnyTLSNode struct { + Port uint16 `json:"port"` + SecurityConfig *SecurityConfig `json:"security_config"` +} + +type TuicNode struct { + Port uint16 `json:"port"` + SecurityConfig *SecurityConfig `json:"security_config"` +} + +type Hysteria2Node struct { + Port uint16 `json:"port"` + HopPorts string `json:"hop_ports"` + HopInterval int `json:"hop_interval"` + ObfsPassword string `json:"obfs_password"` + SecurityConfig *SecurityConfig `json:"security_config"` +} diff --git a/internal/logic/server/getServerConfigLogic.go b/internal/logic/server/getServerConfigLogic.go index 7c8eaa9..94221a9 100644 --- a/internal/logic/server/getServerConfigLogic.go +++ b/internal/logic/server/getServerConfigLogic.go @@ -6,8 +6,8 @@ import ( "fmt" "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/model/node" - "github.com/perfect-panel/server/internal/config" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/logger" @@ -21,7 +21,7 @@ type GetServerConfigLogic struct { svcCtx *svc.ServiceContext } -// Get server config +// NewGetServerConfigLogic Get server config func NewGetServerConfigLogic(ctx *gin.Context, svcCtx *svc.ServiceContext) *GetServerConfigLogic { return &GetServerConfigLogic{ Logger: logger.WithContext(ctx.Request.Context()), @@ -31,7 +31,7 @@ func NewGetServerConfigLogic(ctx *gin.Context, svcCtx *svc.ServiceContext) *GetS } func (l *GetServerConfigLogic) GetServerConfig(req *types.GetServerConfigRequest) (resp *types.GetServerConfigResponse, err error) { - cacheKey := fmt.Sprintf("%s%d", config.ServerConfigCacheKey, req.ServerId) + cacheKey := fmt.Sprintf("%s%d:%s", node.ServerConfigCacheKey, req.ServerId, req.Protocol) cache, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() if err == nil { if cache != "" { @@ -42,7 +42,7 @@ func (l *GetServerConfigLogic) GetServerConfig(req *types.GetServerConfigRequest return nil, xerr.StatusNotModified } l.ctx.Header("ETag", etag) - resp := &types.GetServerConfigResponse{} + resp = &types.GetServerConfigResponse{} err = json.Unmarshal([]byte(cache), resp) if err != nil { l.Errorw("[ServerConfigCacheKey] json unmarshal error", logger.Field("error", err.Error())) @@ -51,21 +51,27 @@ func (l *GetServerConfigLogic) GetServerConfig(req *types.GetServerConfigRequest return resp, nil } } - nodeInfo, err := l.svcCtx.ServerModel.FindOne(l.ctx, req.ServerId) + data, err := l.svcCtx.NodeModel.FindOneServer(l.ctx, req.ServerId) if err != nil { l.Errorw("[GetServerConfig] FindOne error", logger.Field("error", err.Error())) return nil, err } - cfg := make(map[string]interface{}) - err = json.Unmarshal([]byte(nodeInfo.Config), &cfg) - if err != nil { - l.Errorw("[GetServerConfig] json unmarshal error", logger.Field("error", err.Error())) - return nil, err + + // compatible hysteria2, remove in future versions + protocolRequest := req.Protocol + if protocolRequest == Hysteria2 { + protocolRequest = Hysteria } - if nodeInfo.Protocol == "shadowsocks" { - if value, ok := cfg["server_key"]; ok && value != "" { - cfg["server_key"] = base64.StdEncoding.EncodeToString([]byte(value.(string))) + protocols, err := data.UnmarshalProtocols() + if err != nil { + return nil, err + } + var cfg map[string]interface{} + for _, protocol := range protocols { + if protocol.Type == protocolRequest { + cfg = l.compatible(protocol) + break } } @@ -74,18 +80,162 @@ func (l *GetServerConfigLogic) GetServerConfig(req *types.GetServerConfigRequest PullInterval: l.svcCtx.Config.Node.NodePullInterval, PushInterval: l.svcCtx.Config.Node.NodePushInterval, }, - Protocol: nodeInfo.Protocol, + Protocol: req.Protocol, Config: cfg, } - data, err := json.Marshal(resp) + c, err := json.Marshal(resp) if err != nil { l.Errorw("[GetServerConfig] json marshal error", logger.Field("error", err.Error())) return nil, err } - etag := tool.GenerateETag(data) + etag := tool.GenerateETag(c) l.ctx.Header("ETag", etag) - if err = l.svcCtx.Redis.Set(l.ctx, cacheKey, data, -1).Err(); err != nil { + if err = l.svcCtx.Redis.Set(l.ctx, cacheKey, c, -1).Err(); err != nil { l.Errorw("[GetServerConfig] redis set error", logger.Field("error", err.Error())) } + // Check If-None-Match header + match := l.ctx.GetHeader("If-None-Match") + if match == etag { + return nil, xerr.StatusNotModified + } + return resp, nil } + +func (l *GetServerConfigLogic) compatible(config node.Protocol) map[string]interface{} { + var result interface{} + switch config.Type { + case ShadowSocks: + result = ShadowsocksNode{ + Port: config.Port, + Cipher: config.Cipher, + ServerKey: base64.StdEncoding.EncodeToString([]byte(config.ServerKey)), + } + case Vless: + result = VlessNode{ + Port: config.Port, + Flow: config.Flow, + Network: config.Transport, + TransportConfig: &TransportConfig{ + Path: config.Path, + Host: config.Host, + ServiceName: config.ServiceName, + DisableSNI: config.DisableSNI, + ReduceRtt: config.ReduceRtt, + UDPRelayMode: config.UDPRelayMode, + CongestionController: config.CongestionController, + }, + Security: config.Security, + SecurityConfig: &SecurityConfig{ + SNI: config.SNI, + AllowInsecure: &config.AllowInsecure, + Fingerprint: config.Fingerprint, + RealityServerAddress: config.RealityServerAddr, + RealityServerPort: config.RealityServerPort, + RealityPrivateKey: config.RealityPrivateKey, + RealityPublicKey: config.RealityPublicKey, + RealityShortId: config.RealityShortId, + }, + } + case Vmess: + result = VmessNode{ + Port: config.Port, + Network: config.Transport, + TransportConfig: &TransportConfig{ + Path: config.Path, + Host: config.Host, + ServiceName: config.ServiceName, + DisableSNI: config.DisableSNI, + ReduceRtt: config.ReduceRtt, + UDPRelayMode: config.UDPRelayMode, + CongestionController: config.CongestionController, + }, + Security: config.Security, + SecurityConfig: &SecurityConfig{ + SNI: config.SNI, + AllowInsecure: &config.AllowInsecure, + Fingerprint: config.Fingerprint, + RealityServerAddress: config.RealityServerAddr, + RealityServerPort: config.RealityServerPort, + RealityPrivateKey: config.RealityPrivateKey, + RealityPublicKey: config.RealityPublicKey, + RealityShortId: config.RealityShortId, + }, + } + case Trojan: + result = TrojanNode{ + Port: config.Port, + Network: config.Transport, + TransportConfig: &TransportConfig{ + Path: config.Path, + Host: config.Host, + ServiceName: config.ServiceName, + DisableSNI: config.DisableSNI, + ReduceRtt: config.ReduceRtt, + UDPRelayMode: config.UDPRelayMode, + CongestionController: config.CongestionController, + }, + Security: config.Security, + SecurityConfig: &SecurityConfig{ + SNI: config.SNI, + AllowInsecure: &config.AllowInsecure, + Fingerprint: config.Fingerprint, + RealityServerAddress: config.RealityServerAddr, + RealityServerPort: config.RealityServerPort, + RealityPrivateKey: config.RealityPrivateKey, + RealityPublicKey: config.RealityPublicKey, + RealityShortId: config.RealityShortId, + }, + } + case AnyTLS: + result = AnyTLSNode{ + Port: config.Port, + SecurityConfig: &SecurityConfig{ + SNI: config.SNI, + AllowInsecure: &config.AllowInsecure, + Fingerprint: config.Fingerprint, + RealityServerAddress: config.RealityServerAddr, + RealityServerPort: config.RealityServerPort, + RealityPrivateKey: config.RealityPrivateKey, + RealityPublicKey: config.RealityPublicKey, + RealityShortId: config.RealityShortId, + }, + } + case Tuic: + result = TuicNode{ + Port: config.Port, + SecurityConfig: &SecurityConfig{ + SNI: config.SNI, + AllowInsecure: &config.AllowInsecure, + Fingerprint: config.Fingerprint, + RealityServerAddress: config.RealityServerAddr, + RealityServerPort: config.RealityServerPort, + RealityPrivateKey: config.RealityPrivateKey, + RealityPublicKey: config.RealityPublicKey, + RealityShortId: config.RealityShortId, + }, + } + case Hysteria: + result = Hysteria2Node{ + Port: config.Port, + HopPorts: config.HopPorts, + HopInterval: config.HopInterval, + ObfsPassword: config.ObfsPassword, + SecurityConfig: &SecurityConfig{ + SNI: config.SNI, + AllowInsecure: &config.AllowInsecure, + Fingerprint: config.Fingerprint, + RealityServerAddress: config.RealityServerAddr, + RealityServerPort: config.RealityServerPort, + RealityPrivateKey: config.RealityPrivateKey, + RealityPublicKey: config.RealityPublicKey, + RealityShortId: config.RealityShortId, + }, + } + + } + var resp map[string]interface{} + s, _ := json.Marshal(result) + _ = json.Unmarshal(s, &resp) + return resp +} diff --git a/internal/logic/server/getServerUserListLogic.go b/internal/logic/server/getServerUserListLogic.go index bf4a6a3..70ea51f 100644 --- a/internal/logic/server/getServerUserListLogic.go +++ b/internal/logic/server/getServerUserListLogic.go @@ -3,10 +3,12 @@ package server import ( "encoding/json" "fmt" + "strings" "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/internal/model/subscribe" - "github.com/perfect-panel/server/internal/config" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/logger" @@ -21,7 +23,7 @@ type GetServerUserListLogic struct { svcCtx *svc.ServiceContext } -// Get user list +// NewGetServerUserListLogic Get user list func NewGetServerUserListLogic(ctx *gin.Context, svcCtx *svc.ServiceContext) *GetServerUserListLogic { return &GetServerUserListLogic{ Logger: logger.WithContext(ctx.Request.Context()), @@ -31,30 +33,53 @@ func NewGetServerUserListLogic(ctx *gin.Context, svcCtx *svc.ServiceContext) *Ge } func (l *GetServerUserListLogic) GetServerUserList(req *types.GetServerUserListRequest) (resp *types.GetServerUserListResponse, err error) { - cacheKey := fmt.Sprintf("%s%d", config.ServerUserListCacheKey, req.ServerId) + cacheKey := fmt.Sprintf("%s%d", node.ServerUserListCacheKey, req.ServerId) cache, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() - if err == nil { - if cache != "" { - etag := tool.GenerateETag([]byte(cache)) - resp := &types.GetServerUserListResponse{} - // Check If-None-Match header - if match := l.ctx.GetHeader("If-None-Match"); match == etag { - return nil, xerr.StatusNotModified - } - l.ctx.Header("ETag", etag) - err = json.Unmarshal([]byte(cache), resp) - if err != nil { - l.Errorw("[ServerUserListCacheKey] json unmarshal error", logger.Field("error", err.Error())) - return nil, err - } - return resp, nil + if cache != "" { + etag := tool.GenerateETag([]byte(cache)) + resp = &types.GetServerUserListResponse{} + // Check If-None-Match header + if match := l.ctx.GetHeader("If-None-Match"); match == etag { + return nil, xerr.StatusNotModified } + l.ctx.Header("ETag", etag) + err = json.Unmarshal([]byte(cache), resp) + if err != nil { + l.Errorw("[ServerUserListCacheKey] json unmarshal error", logger.Field("error", err.Error())) + return nil, err + } + return resp, nil } - server, err := l.svcCtx.ServerModel.FindOne(l.ctx, req.ServerId) + server, err := l.svcCtx.NodeModel.FindOneServer(l.ctx, req.ServerId) if err != nil { return nil, err } - subs, err := l.svcCtx.SubscribeModel.QuerySubscribeIdsByServerIdAndServerGroupId(l.ctx, server.Id, server.GroupId) + + _, nodes, err := l.svcCtx.NodeModel.FilterNodeList(l.ctx, &node.FilterNodeParams{ + Page: 1, + Size: 1000, + ServerId: []int64{server.Id}, + Protocol: req.Protocol, + }) + if err != nil { + l.Errorw("FilterNodeList error", logger.Field("error", err.Error())) + return nil, err + } + var nodeTag []string + var nodeIds []int64 + for _, n := range nodes { + nodeIds = append(nodeIds, n.Id) + if n.Tags != "" { + nodeTag = append(nodeTag, strings.Split(n.Tags, ",")...) + } + } + + _, subs, err := l.svcCtx.SubscribeModel.FilterList(l.ctx, &subscribe.FilterParams{ + Page: 1, + Size: 9999, + Node: nodeIds, + Tags: nodeTag, + }) if err != nil { l.Errorw("QuerySubscribeIdsByServerIdAndServerGroupId error", logger.Field("error", err.Error())) return nil, err @@ -76,16 +101,10 @@ func (l *GetServerUserListLogic) GetServerUserList(req *types.GetServerUserListR return nil, err } for _, datum := range data { - speedLimit := server.SpeedLimit - if (int(sub.SpeedLimit) < server.SpeedLimit && sub.SpeedLimit != 0) || - (int(sub.SpeedLimit) > server.SpeedLimit && sub.SpeedLimit == 0) { - speedLimit = int(sub.SpeedLimit) - } - users = append(users, types.ServerUser{ Id: datum.Id, UUID: datum.UUID, - SpeedLimit: int64(speedLimit), + SpeedLimit: sub.SpeedLimit, DeviceLimit: sub.DeviceLimit, }) } @@ -106,5 +125,9 @@ func (l *GetServerUserListLogic) GetServerUserList(req *types.GetServerUserListR if err != nil { l.Errorw("[ServerUserListCacheKey] redis set error", logger.Field("error", err.Error())) } + // Check If-None-Match header + if match := l.ctx.GetHeader("If-None-Match"); match == etag { + return nil, xerr.StatusNotModified + } return resp, nil } diff --git a/internal/logic/server/pushOnlineUsersLogic.go b/internal/logic/server/pushOnlineUsersLogic.go index b8ce501..5b17656 100644 --- a/internal/logic/server/pushOnlineUsersLogic.go +++ b/internal/logic/server/pushOnlineUsersLogic.go @@ -5,7 +5,7 @@ import ( "errors" "fmt" - "github.com/perfect-panel/server/internal/model/cache" + "github.com/perfect-panel/server/internal/model/node" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/logger" @@ -40,26 +40,30 @@ func (l *PushOnlineUsersLogic) PushOnlineUsers(req *types.OnlineUsersRequest) er } // Find server info - _, err := l.svcCtx.ServerModel.FindOne(l.ctx, req.ServerId) + _, err := l.svcCtx.NodeModel.FindOneServer(l.ctx, req.ServerId) if err != nil { l.Errorw("[PushOnlineUsers] FindOne error", logger.Field("error", err)) return fmt.Errorf("server not found: %w", err) } - userOnlineIp := make([]cache.NodeOnlineUser, 0) + onlineUsers := make(node.OnlineUserSubscribe) for _, user := range req.Users { - userOnlineIp = append(userOnlineIp, cache.NodeOnlineUser{ - SID: user.SID, - IP: user.IP, - }) + if online, ok := onlineUsers[user.SID]; ok { + // If user already exists, update IP if different + online = append(online, user.IP) + onlineUsers[user.SID] = online + } else { + // New user, add to map + onlineUsers[user.SID] = []string{user.IP} + } } - err = l.svcCtx.NodeCache.AddOnlineUserIP(l.ctx, userOnlineIp) + err = l.svcCtx.NodeModel.UpdateOnlineUserSubscribe(l.ctx, req.ServerId, req.Protocol, onlineUsers) if err != nil { l.Errorw("[PushOnlineUsers] cache operation error", logger.Field("error", err)) return err } - err = l.svcCtx.NodeCache.UpdateNodeOnlineUser(l.ctx, req.ServerId, userOnlineIp) + err = l.svcCtx.NodeModel.UpdateOnlineUserSubscribeGlobal(l.ctx, onlineUsers) if err != nil { l.Errorw("[PushOnlineUsers] cache operation error", logger.Field("error", err)) diff --git a/internal/logic/server/queryServerProtocolConfigLogic.go b/internal/logic/server/queryServerProtocolConfigLogic.go new file mode 100644 index 0000000..4521c55 --- /dev/null +++ b/internal/logic/server/queryServerProtocolConfigLogic.go @@ -0,0 +1,93 @@ +package server + +import ( + "context" + + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" +) + +type QueryServerProtocolConfigLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewQueryServerProtocolConfigLogic Get Server Protocol Config +func NewQueryServerProtocolConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryServerProtocolConfigLogic { + return &QueryServerProtocolConfigLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryServerProtocolConfigLogic) QueryServerProtocolConfig(req *types.QueryServerConfigRequest) (resp *types.QueryServerConfigResponse, err error) { + // find server + data, err := l.svcCtx.NodeModel.FindOneServer(l.ctx, req.ServerID) + if err != nil { + l.Errorf("[GetServerProtocols] FindOneServer Error: %s", err.Error()) + return nil, err + } + + // handler protocols + var protocols []types.Protocol + dst, err := data.UnmarshalProtocols() + if err != nil { + l.Errorf("[FilterServerList] UnmarshalProtocols Error: %s", err.Error()) + return nil, err + } + tool.DeepCopy(&protocols, dst) + + // filter by req.Protocols + + if len(req.Protocols) > 0 { + var filtered []types.Protocol + protocolSet := make(map[string]struct{}) + for _, p := range req.Protocols { + protocolSet[p] = struct{}{} + } + for _, p := range protocols { + if _, exists := protocolSet[p.Type]; exists { + filtered = append(filtered, p) + } + } + protocols = filtered + } + + var dns []types.NodeDNS + if len(l.svcCtx.Config.Node.DNS) > 0 { + for _, d := range l.svcCtx.Config.Node.DNS { + dns = append(dns, types.NodeDNS{ + Proto: d.Proto, + Address: d.Address, + Domains: d.Domains, + }) + } + } + var outbound []types.NodeOutbound + if len(l.svcCtx.Config.Node.Outbound) > 0 { + for _, o := range l.svcCtx.Config.Node.Outbound { + outbound = append(outbound, types.NodeOutbound{ + Name: o.Name, + Protocol: o.Protocol, + Address: o.Address, + Port: o.Port, + Password: o.Password, + Rules: o.Rules, + }) + } + } + + return &types.QueryServerConfigResponse{ + TrafficReportThreshold: l.svcCtx.Config.Node.TrafficReportThreshold, + IPStrategy: l.svcCtx.Config.Node.IPStrategy, + DNS: dns, + Block: l.svcCtx.Config.Node.Block, + Outbound: outbound, + Protocols: protocols, + Total: int64(len(protocols)), + }, nil +} diff --git a/internal/logic/server/serverPushStatusLogic.go b/internal/logic/server/serverPushStatusLogic.go index 7e5ff91..d7c0bad 100644 --- a/internal/logic/server/serverPushStatusLogic.go +++ b/internal/logic/server/serverPushStatusLogic.go @@ -3,8 +3,9 @@ package server import ( "context" "errors" + "time" - "github.com/perfect-panel/server/internal/model/cache" + "github.com/perfect-panel/server/internal/model/node" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/logger" @@ -16,7 +17,7 @@ type ServerPushStatusLogic struct { svcCtx *svc.ServiceContext } -// Push server status +// NewServerPushStatusLogic Push server status func NewServerPushStatusLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ServerPushStatusLogic { return &ServerPushStatusLogic{ Logger: logger.WithContext(ctx), @@ -27,12 +28,12 @@ func NewServerPushStatusLogic(ctx context.Context, svcCtx *svc.ServiceContext) * func (l *ServerPushStatusLogic) ServerPushStatus(req *types.ServerPushStatusRequest) error { // Find server info - serverInfo, err := l.svcCtx.ServerModel.FindOne(l.ctx, req.ServerId) + serverInfo, err := l.svcCtx.NodeModel.FindOneServer(l.ctx, req.ServerId) if err != nil || serverInfo.Id <= 0 { l.Errorw("[PushOnlineUsers] FindOne error", logger.Field("error", err)) return errors.New("server not found") } - err = l.svcCtx.NodeCache.UpdateNodeStatus(l.ctx, req.ServerId, cache.NodeStatus{ + err = l.svcCtx.NodeModel.UpdateStatusCache(l.ctx, req.ServerId, &node.Status{ Cpu: req.Cpu, Mem: req.Mem, Disk: req.Disk, @@ -42,5 +43,14 @@ func (l *ServerPushStatusLogic) ServerPushStatus(req *types.ServerPushStatusRequ l.Errorw("[ServerPushStatus] UpdateNodeStatus error", logger.Field("error", err)) return errors.New("update node status failed") } + now := time.Now() + serverInfo.LastReportedAt = &now + + err = l.svcCtx.NodeModel.UpdateServer(l.ctx, serverInfo) + if err != nil { + l.Errorw("[ServerPushStatus] UpdateServer error", logger.Field("error", err)) + return nil + } + return nil } diff --git a/internal/logic/server/serverPushUserTrafficLogic.go b/internal/logic/server/serverPushUserTrafficLogic.go index 3481340..c6ab4e6 100644 --- a/internal/logic/server/serverPushUserTrafficLogic.go +++ b/internal/logic/server/serverPushUserTrafficLogic.go @@ -3,9 +3,9 @@ package server import ( "context" "encoding/json" + "time" "github.com/hibiken/asynq" - "github.com/perfect-panel/server/internal/model/cache" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/logger" @@ -32,7 +32,7 @@ func NewServerPushUserTrafficLogic(ctx context.Context, svcCtx *svc.ServiceConte func (l *ServerPushUserTrafficLogic) ServerPushUserTraffic(req *types.ServerPushUserTrafficRequest) error { // Find server info - serverInfo, err := l.svcCtx.ServerModel.FindOne(l.ctx, req.ServerId) + serverInfo, err := l.svcCtx.NodeModel.FindOneServer(l.ctx, req.ServerId) if err != nil { l.Errorw("[PushOnlineUsers] FindOne error", logger.Field("error", err)) return errors.New("server not found") @@ -40,23 +40,10 @@ func (l *ServerPushUserTrafficLogic) ServerPushUserTraffic(req *types.ServerPush // Create traffic task var request task.TrafficStatistics - var userTraffic []cache.UserTraffic request.ServerId = serverInfo.Id + request.Protocol = req.Protocol tool.DeepCopy(&request.Logs, req.Traffic) - tool.DeepCopy(&userTraffic, req.Traffic) - // update today traffic rank - err = l.svcCtx.NodeCache.AddNodeTodayTraffic(l.ctx, serverInfo.Id, userTraffic) - if err != nil { - l.Errorw("[ServerPushUserTraffic] AddNodeTodayTraffic error", logger.Field("error", err)) - return errors.New("add node today traffic error") - } - for _, user := range req.Traffic { - if err = l.svcCtx.NodeCache.AddUserTodayTraffic(l.ctx, user.SID, user.Upload, user.Download); err != nil { - l.Errorw("[ServerPushUserTraffic] AddUserTodayTraffic error", logger.Field("error", err)) - continue - } - } // Push traffic task val, _ := json.Marshal(request) t := asynq.NewTask(task.ForthwithTrafficStatistics, val, asynq.MaxRetry(3)) @@ -66,5 +53,15 @@ func (l *ServerPushUserTrafficLogic) ServerPushUserTraffic(req *types.ServerPush } else { l.Infow("[ServerPushUserTraffic] Push traffic task success", logger.Field("task", t), logger.Field("info", info)) } + + // Update server last reported time + now := time.Now() + serverInfo.LastReportedAt = &now + + err = l.svcCtx.NodeModel.UpdateServer(l.ctx, serverInfo) + if err != nil { + l.Errorw("[ServerPushUserTraffic] UpdateServer error", logger.Field("error", err)) + return nil + } return nil } diff --git a/internal/logic/subscribe/subscribeLogic.go b/internal/logic/subscribe/subscribeLogic.go index 6e8f763..6a4a6e5 100644 --- a/internal/logic/subscribe/subscribeLogic.go +++ b/internal/logic/subscribe/subscribeLogic.go @@ -6,12 +6,10 @@ import ( "strings" "time" - "github.com/perfect-panel/server/pkg/adapter" - "github.com/perfect-panel/server/pkg/adapter/shadowrocket" - "github.com/perfect-panel/server/pkg/adapter/surfboard" - "github.com/perfect-panel/server/pkg/adapter/surge" - - "github.com/perfect-panel/server/internal/model/server" + "github.com/perfect-panel/server/adapter" + "github.com/perfect-panel/server/internal/model/client" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/model/node" "github.com/perfect-panel/server/internal/model/user" @@ -39,37 +37,123 @@ func NewSubscribeLogic(ctx *gin.Context, svc *svc.ServiceContext) *SubscribeLogi } } -func (l *SubscribeLogic) Generate(req *types.SubscribeRequest) (*types.SubscribeResponse, error) { - userSub, err := l.getUserSubscribe(req.Token) +func (l *SubscribeLogic) Handler(req *types.SubscribeRequest) (resp *types.SubscribeResponse, err error) { + // query client list + clients, err := l.svc.ClientModel.List(l.ctx.Request.Context()) if err != nil { + l.Errorw("[SubscribeLogic] Query client list failed", logger.Field("error", err.Error())) + return nil, err + } + + userAgent := strings.ToLower(l.ctx.Request.UserAgent()) + + var targetApp, defaultApp *client.SubscribeApplication + + for _, item := range clients { + u := strings.ToLower(item.UserAgent) + if item.IsDefault { + defaultApp = item + } + + if strings.Contains(userAgent, u) { + // Special handling for Stash + if strings.Contains(userAgent, "stash") && !strings.Contains(u, "stash") { + continue + } + targetApp = item + break + } + } + if targetApp == nil { + l.Debugf("[SubscribeLogic] No matching client found", logger.Field("userAgent", userAgent)) + if defaultApp == nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "No matching client found for user agent: %s", userAgent) + } + targetApp = defaultApp + } + // Find user subscribe by token + userSubscribe, err := l.getUserSubscribe(req.Token) + if err != nil { + l.Errorw("[SubscribeLogic] Get user subscribe failed", logger.Field("error", err.Error()), logger.Field("token", req.Token)) return nil, err } var subscribeStatus = false defer func() { - l.logSubscribeActivity(subscribeStatus, userSub, req) + l.logSubscribeActivity(subscribeStatus, userSubscribe, req) }() + // find subscribe info + subscribeInfo, err := l.svc.SubscribeModel.FindOne(l.ctx.Request.Context(), userSubscribe.SubscribeId) + if err != nil { + l.Errorw("[SubscribeLogic] Find subscribe info failed", logger.Field("error", err.Error()), logger.Field("subscribeId", userSubscribe.SubscribeId)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Find subscribe info failed: %v", err.Error()) + } - servers, err := l.getServers(userSub) + // Find server list by user subscribe + servers, err := l.getServers(userSubscribe) if err != nil { return nil, err } + a := adapter.NewAdapter( + targetApp.SubscribeTemplate, + adapter.WithServers(servers), + adapter.WithSiteName(l.svc.Config.Site.SiteName), + adapter.WithSubscribeName(subscribeInfo.Name), + adapter.WithOutputFormat(targetApp.OutputFormat), + adapter.WithUserInfo(adapter.User{ + Password: userSubscribe.UUID, + ExpiredAt: userSubscribe.ExpireTime, + Download: userSubscribe.Download, + Upload: userSubscribe.Upload, + Traffic: userSubscribe.Traffic, + SubscribeURL: l.getSubscribeV2URL(req.Token), + }), + ) - rules, err := l.getRules() + // Get client config + adapterClient, err := a.Client() if err != nil { - return nil, err + l.Errorw("[SubscribeLogic] Client error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(500), "Client error: %v", err.Error()) + } + bytes, err := adapterClient.Build() + if err != nil { + l.Errorw("[SubscribeLogic] Build client config failed", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(500), "Build client config failed: %v", err.Error()) } - resp, headerInfo, err := l.buildClientConfig(req, userSub, servers, rules) - if err != nil { - return nil, err + var formats = []string{"json", "yaml", "conf"} + + for _, format := range formats { + if format == strings.ToLower(targetApp.OutputFormat) { + l.ctx.Header("content-disposition", fmt.Sprintf("attachment;filename*=UTF-8''%s.%s", url.QueryEscape(l.svc.Config.Site.SiteName), format)) + l.ctx.Header("Content-Type", "application/octet-stream; charset=UTF-8") + + } } + resp = &types.SubscribeResponse{ + Config: bytes, + Header: fmt.Sprintf( + "upload=%d;download=%d;total=%d;expire=%d", + userSubscribe.Upload, userSubscribe.Download, userSubscribe.Traffic, userSubscribe.ExpireTime.Unix(), + ), + } subscribeStatus = true - return &types.SubscribeResponse{ - Config: resp, - Header: headerInfo, - }, nil + return +} + +func (l *SubscribeLogic) getSubscribeV2URL(token string) string { + if l.svc.Config.Subscribe.PanDomain { + return fmt.Sprintf("https://%s", l.ctx.Request.Host) + } + + if l.svc.Config.Subscribe.SubscribeDomain != "" { + domains := strings.Split(l.svc.Config.Subscribe.SubscribeDomain, "\n") + return fmt.Sprintf("https://%s%s?token=%s", domains[0], l.svc.Config.Subscribe.SubscribePath, token) + } + + return fmt.Sprintf("https://%s%s?token=%s&", l.ctx.Request.Host, l.svc.Config.Subscribe.SubscribePath, token) } func (l *SubscribeLogic) getUserSubscribe(token string) (*user.Subscribe, error) { @@ -79,10 +163,11 @@ func (l *SubscribeLogic) getUserSubscribe(token string) (*user.Subscribe, error) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe error: %v", err.Error()) } - if userSub.Status > 1 { - l.Infow("[Generate Subscribe]subscribe is not available", logger.Field("status", int(userSub.Status)), logger.Field("token", token)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SubscribeNotAvailable), "subscribe is not available") - } + // Ignore expiration check + //if userSub.Status > 1 { + // l.Infow("[Generate Subscribe]subscribe is not available", logger.Field("status", int(userSub.Status)), logger.Field("token", token)) + // return nil, errors.Wrapf(xerr.NewErrCode(xerr.SubscribeNotAvailable), "subscribe is not available") + //} return userSub, nil } @@ -92,19 +177,27 @@ func (l *SubscribeLogic) logSubscribeActivity(subscribeStatus bool, userSub *use return } - err := l.svc.UserModel.InsertSubscribeLog(l.ctx.Request.Context(), &user.SubscribeLog{ - UserId: userSub.UserId, - UserSubscribeId: userSub.Id, + subscribeLog := log.Subscribe{ Token: req.Token, - IP: l.ctx.ClientIP(), - UserAgent: l.ctx.Request.UserAgent(), + UserAgent: req.UA, + ClientIP: l.ctx.ClientIP(), + UserSubscribeId: userSub.Id, + } + + content, _ := subscribeLog.Marshal() + + err := l.svc.LogModel.Insert(l.ctx.Request.Context(), &log.SystemLog{ + Type: log.TypeSubscribe.Uint8(), + ObjectID: userSub.UserId, // log user id + Date: time.Now().Format(time.DateOnly), + Content: string(content), }) if err != nil { l.Errorw("[Generate Subscribe]insert subscribe log error: %v", logger.Field("error", err.Error())) } } -func (l *SubscribeLogic) getServers(userSub *user.Subscribe) ([]*server.Server, error) { +func (l *SubscribeLogic) getServers(userSub *user.Subscribe) ([]*node.Node, error) { if l.isSubscriptionExpired(userSub) { return l.createExpiredServers(), nil } @@ -115,86 +208,66 @@ func (l *SubscribeLogic) getServers(userSub *user.Subscribe) ([]*server.Server, return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe details error: %v", err.Error()) } - serverIds := tool.StringToInt64Slice(subDetails.Server) - groupIds := tool.StringToInt64Slice(subDetails.ServerGroup) + nodeIds := tool.StringToInt64Slice(subDetails.Nodes) + tags := strings.Split(subDetails.NodeTags, ",") - // 🔍 订阅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]nodes: %v, NodeTags: %v", nodeIds, tags) - l.Debugf("[Generate Subscribe]serverIds: %v, groupIds: %v", serverIds, groupIds) + enable := true - // 查询所有服务器用于调试 - 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) - } - } + _, nodes, err := l.svc.NodeModel.FilterNodeList(l.ctx.Request.Context(), &node.FilterNodeParams{ + Page: 1, + Size: 1000, + NodeId: nodeIds, + Tag: tool.RemoveDuplicateElements(tags...), + Preload: true, + Enabled: &enable, // Only get enabled nodes + }) - 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(nodes)) if err != nil { l.Errorw("[Generate Subscribe]find server details error: %v", logger.Field("error", err.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find server details error: %v", err.Error()) } - logger.Debugf("[Generate Subscribe]found servers: %v", len(servers)) - return servers, nil + logger.Debugf("[Generate Subscribe]found servers: %v", len(nodes)) + return nodes, nil } func (l *SubscribeLogic) isSubscriptionExpired(userSub *user.Subscribe) bool { return userSub.ExpireTime.Unix() < time.Now().Unix() && userSub.ExpireTime.Unix() != 0 } -func (l *SubscribeLogic) createExpiredServers() []*server.Server { +func (l *SubscribeLogic) createExpiredServers() []*node.Node { enable := true host := l.getFirstHostLine() - return []*server.Server{ + return []*node.Node{ { - Name: "Subscribe Expired", - ServerAddr: "127.0.0.1", - RelayMode: "none", - Protocol: "shadowsocks", - Config: "{\"method\":\"aes-256-gcm\",\"port\":1}", - Enable: &enable, - Sort: 0, + Name: "Subscribe Expired", + Tags: "", + Port: 18080, + Address: "127.0.0.1", + Server: &node.Server{ + Id: 1, + Name: "Subscribe Expired", + Protocols: "[{\"type\":\"shadowsocks\",\"cipher\":\"aes-256-gcm\",\"port\":1}]", + }, + Protocol: "shadowsocks", + Enabled: &enable, }, { - Name: host, - ServerAddr: "127.0.0.1", - RelayMode: "none", - Protocol: "shadowsocks", - Config: "{\"method\":\"aes-256-gcm\",\"port\":1}", - Enable: &enable, - Sort: 0, + Name: host, + Tags: "", + Port: 18080, + Address: "127.0.0.1", + Server: &node.Server{ + Id: 1, + Name: "Subscribe Expired", + Protocols: "[{\"type\":\"shadowsocks\",\"cipher\":\"aes-256-gcm\",\"port\":1}]", + }, + Protocol: "shadowsocks", + Enabled: &enable, }, } } @@ -207,175 +280,3 @@ func (l *SubscribeLogic) getFirstHostLine() string { } return host } - -func (l *SubscribeLogic) getRules() ([]*server.RuleGroup, error) { - rules, err := l.svc.ServerModel.QueryAllRuleGroup(l.ctx) - if err != nil { - l.Errorw("[Generate Subscribe]find rule group error: %v", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find rule group error: %v", err.Error()) - } - return rules, nil -} - -func (l *SubscribeLogic) buildClientConfig(req *types.SubscribeRequest, userSub *user.Subscribe, servers []*server.Server, rules []*server.RuleGroup) ([]byte, string, error) { - tags := make(map[string][]*server.Server) - - serverTags, err := l.svc.ServerModel.FindServerTags(l.ctx) - if err != nil { - l.Errorw("[Generate Subscribe]find server tags error: %v", logger.Field("error", err.Error())) - return nil, "", errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find server tags error: %v", err.Error()) - } - // Deduplicate tags - serverTags = tool.RemoveDuplicateElements(serverTags...) - for _, tag := range serverTags { - s, err := l.svc.ServerModel.FindServersByTag(l.ctx.Request.Context(), tag) - if err != nil { - l.Errorw("[Generate Subscribe]find servers by tag error: %v", logger.Field("error", err.Error())) - continue - } - if len(s) > 0 { - tags[tag] = s - } - } - - proxyManager := adapter.NewAdapter(&adapter.Config{ - Nodes: servers, - Rules: rules, - Tags: tags, - }) - clientType := l.getClientType(req) - var resp []byte - - l.Logger.Info(fmt.Sprintf("[Generate Subscribe] %s", clientType), logger.Field("ua", req.UA), logger.Field("flag", req.Flag)) - - switch clientType { - case "clash": - resp, err = proxyManager.BuildClash(userSub.UUID) - if err != nil { - l.Errorw("[Generate Subscribe] build clash error", logger.Field("error", err.Error())) - return nil, "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "build clash error: %v", err.Error()) - } - l.setClashHeaders() - case "sing-box": - resp, err = proxyManager.BuildSingbox(userSub.UUID) - if err != nil { - l.Errorw("[Generate Subscribe] build sing-box error", logger.Field("error", err.Error())) - return nil, "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "build sing-box error: %v", err.Error()) - } - case "quantumult": - resp = []byte(proxyManager.BuildQuantumultX(userSub.UUID)) - case "shadowrocket": - resp = proxyManager.BuildShadowrocket(userSub.UUID, shadowrocket.UserInfo{ - Upload: userSub.Upload, - Download: userSub.Download, - TotalTraffic: userSub.Traffic, - ExpiredDate: userSub.ExpireTime, - }) - case "loon": - resp = proxyManager.BuildLoon(userSub.UUID) - l.setLoonHeaders() - case "surfboard": - subsURL := l.getSubscribeURL(userSub.Token, "surfboard") - resp = proxyManager.BuildSurfboard(l.svc.Config.Site.SiteName, surfboard.UserInfo{ - Upload: userSub.Upload, - Download: userSub.Download, - TotalTraffic: userSub.Traffic, - ExpiredDate: userSub.ExpireTime, - UUID: userSub.UUID, - SubscribeURL: subsURL, - }) - l.setSurfboardHeaders() - case "v2rayn": - resp = proxyManager.BuildV2rayN(userSub.UUID) - case "surge": - subsURL := l.getSubscribeURL(userSub.Token, "surge") - resp = proxyManager.BuildSurge(l.svc.Config.Site.SiteName, surge.UserInfo{ - UUID: userSub.UUID, - Upload: userSub.Upload, - Download: userSub.Download, - TotalTraffic: userSub.Traffic, - ExpiredDate: userSub.ExpireTime, - SubscribeURL: subsURL, - }) - l.setSurgeHeaders() - default: - resp = proxyManager.BuildGeneral(userSub.UUID) - } - - headerInfo := fmt.Sprintf("upload=%d;download=%d;total=%d;expire=%d", - userSub.Upload, userSub.Download, userSub.Traffic, userSub.ExpireTime.Unix()) - - return resp, headerInfo, nil -} - -func (l *SubscribeLogic) setClashHeaders() { - l.ctx.Header("content-disposition", fmt.Sprintf("attachment;filename*=UTF-8''%s", url.QueryEscape(l.svc.Config.Site.SiteName))) - l.ctx.Header("Profile-Update-Interval", "24") - l.ctx.Header("Content-Type", "application/octet-stream; charset=UTF-8") -} - -func (l *SubscribeLogic) setSurfboardHeaders() { - 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) setSurgeHeaders() { - l.ctx.Header("content-disposition", fmt.Sprintf("attachment;filename*=UTF-8''%s.conf", url.QueryEscape(l.svc.Config.Site.SiteName))) - l.ctx.Header("Content-Type", "application/octet-stream; charset=UTF-8") -} - -func (l *SubscribeLogic) setLoonHeaders() { - l.ctx.Header("content-disposition", fmt.Sprintf("attachment;filename*=UTF-8''%s.conf", url.QueryEscape(l.svc.Config.Site.SiteName))) - l.ctx.Header("Content-Type", "application/octet-stream; charset=UTF-8") -} - -func (l *SubscribeLogic) getSubscribeURL(token, flag string) string { - if l.svc.Config.Subscribe.PanDomain { - return fmt.Sprintf("https://%s", l.ctx.Request.Host) - } - - if l.svc.Config.Subscribe.SubscribeDomain != "" { - domains := strings.Split(l.svc.Config.Subscribe.SubscribeDomain, "\n") - return fmt.Sprintf("https://%s%s?token=%s&flag=%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) -} - -func (l *SubscribeLogic) getClientType(req *types.SubscribeRequest) string { - clientTypeMap := map[string]string{ - "clash": "clash", - "meta": "clash", - "sing-box": "sing-box", - "hiddify": "sing-box", - "surge": "surge", - "quantumult": "quantumult", - "shadowrocket": "shadowrocket", - "loon": "loon", - "surfboard": "surfboard", - "v2rayn": "v2rayn", - } - - findClient := func(s string) string { - s = strings.ToLower(strings.TrimSpace(s)) - if s == "" { - return "" - } - - for key, clientType := range clientTypeMap { - if strings.Contains(s, key) { - return clientType - } - } - - return "" - } - - // 优先检查Flag参数 - if typ := findClient(req.Flag); typ != "" { - return typ - } - - // 其次检查UA参数 - return findClient(req.UA) -} diff --git a/internal/middleware/authMiddleware.go b/internal/middleware/authMiddleware.go index a76d0ee..fbf3758 100644 --- a/internal/middleware/authMiddleware.go +++ b/internal/middleware/authMiddleware.go @@ -40,6 +40,11 @@ func AuthMiddleware(svc *svc.ServiceContext) func(c *gin.Context) { c.Abort() return } + + loginType := "" + if claims["LoginType"] != nil { + loginType = claims["LoginType"].(string) + } // get user id from token userId := int64(claims["UserId"].(float64)) // get session id from token @@ -77,6 +82,7 @@ func AuthMiddleware(svc *svc.ServiceContext) func(c *gin.Context) { c.Abort() return } + ctx = context.WithValue(ctx, constant.LoginType, loginType) ctx = context.WithValue(ctx, constant.CtxKeyUser, userInfo) ctx = context.WithValue(ctx, constant.CtxKeySessionID, sessionId) c.Request = c.Request.WithContext(ctx) diff --git a/internal/middleware/appMiddleware.go b/internal/middleware/deviceMiddleware.go similarity index 86% rename from internal/middleware/appMiddleware.go rename to internal/middleware/deviceMiddleware.go index c480f22..b66ccb0 100644 --- a/internal/middleware/appMiddleware.go +++ b/internal/middleware/deviceMiddleware.go @@ -3,6 +3,7 @@ package middleware import ( "bufio" "bytes" + "context" "encoding/json" "fmt" "io" @@ -10,29 +11,48 @@ import ( "net/http" "strings" - "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/internal/svc" + pkgaes "github.com/perfect-panel/server/pkg/aes" + "github.com/perfect-panel/server/pkg/constant" "github.com/perfect-panel/server/pkg/result" "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "github.com/gin-gonic/gin" - "github.com/perfect-panel/server/internal/svc" - pkgaes "github.com/perfect-panel/server/pkg/aes" ) const ( noWritten = -1 defaultStatus = http.StatusOK - key = "123456" ) -func AppMiddleware(svc *svc.ServiceContext) func(c *gin.Context) { +func DeviceMiddleware(srvCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - if !strings.Contains(c.Request.URL.Path, "/v1/app") { + + if !srvCtx.Config.Device.Enable { c.Next() return } - rw := NewResponseWriter(c, svc) + + if srvCtx.Config.Device.SecuritySecret == "" { + result.HttpResult(c, nil, errors.Wrapf(xerr.NewErrCode(xerr.SecretIsEmpty), "Secret is empty")) + c.Abort() + return + } + + ctx := c.Request.Context() + if ctx.Value(constant.CtxKeyUser) == nil && c.GetHeader("Login-Type") != "" { + ctx = context.WithValue(ctx, constant.LoginType, c.GetHeader("Login-Type")) + c.Request = c.Request.WithContext(ctx) + } + + loginType, ok := ctx.Value(constant.LoginType).(string) + if !ok || loginType != "device" { + c.Next() + return + } + + rw := NewResponseWriter(c, srvCtx) if !rw.Decrypt() { result.HttpResult(c, nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidCiphertext), "Invalid ciphertext")) c.Abort() @@ -50,17 +70,10 @@ func NewResponseWriter(c *gin.Context, srvCtx *svc.ServiceContext) (rw *Response body: new(bytes.Buffer), ResponseWriter: c.Writer, } - applicationConfig, err := srvCtx.ApplicationModel.FindOneConfig(c, 1) - if err != nil { - logger.Errorf("[AppMiddleware] find application config error: %v", err.Error()) - return - } - if strings.ToUpper(applicationConfig.EncryptionMethod) == "AES" && applicationConfig.EncryptionKey != "" { - rw.encryptionKey = applicationConfig.EncryptionKey - rw.encryptionMethod = applicationConfig.EncryptionMethod - rw.encryption = true - } - return + rw.encryptionKey = srvCtx.Config.Device.SecuritySecret + rw.encryptionMethod = "AES" + rw.encryption = true + return rw } func (rw *ResponseWriter) Encrypt() { diff --git a/internal/middleware/loggerMiddleware.go b/internal/middleware/loggerMiddleware.go index 43e27ab..7bb2def 100644 --- a/internal/middleware/loggerMiddleware.go +++ b/internal/middleware/loggerMiddleware.go @@ -44,6 +44,9 @@ func LoggerMiddleware(svc *svc.ServiceContext) func(c *gin.Context) { // Start recording logs cost := time.Since(start) responseStatus := c.Writer.Status() + + host := c.Request.Host + logs := []logger.LogField{ { Key: "status", @@ -51,7 +54,7 @@ func LoggerMiddleware(svc *svc.ServiceContext) func(c *gin.Context) { }, { Key: "request", - Value: c.Request.Method + " " + c.Request.URL.String(), + Value: c.Request.Method + " " + host + c.Request.URL.String(), }, { Key: "query", @@ -88,6 +91,10 @@ func LoggerMiddleware(svc *svc.ServiceContext) func(c *gin.Context) { } else { logger.WithContext(c.Request.Context()).Infow("HTTP Request", logs...) } + + if responseStatus == 404 { + logger.WithContext(c.Request.Context()).Debugf("404 Not Found: Host:%s Path:%s IsPanDomain:%v", host, c.Request.URL.Path, svc.Config.Subscribe.PanDomain) + } } } diff --git a/internal/middleware/panDomainMiddleware.go b/internal/middleware/panDomainMiddleware.go index f5cbb44..518c787 100644 --- a/internal/middleware/panDomainMiddleware.go +++ b/internal/middleware/panDomainMiddleware.go @@ -1,17 +1,60 @@ package middleware import ( + "net/http" "strings" "github.com/gin-gonic/gin" "github.com/perfect-panel/server/internal/logic/subscribe" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" ) func PanDomainMiddleware(svc *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - if svc.Config.Subscribe.PanDomain { + + if svc.Config.Subscribe.PanDomain && c.Request.URL.Path == "/" { + // intercept browser + ua := c.GetHeader("User-Agent") + + if svc.Config.Subscribe.UserAgentLimit { + if ua == "" { + c.String(http.StatusForbidden, "Access denied") + c.Abort() + return + } + browserKeywords := tool.RemoveDuplicateElements(strings.Split(svc.Config.Subscribe.UserAgentList, "\n")...) + var allow = false + + // query client list + clients, err := svc.ClientModel.List(c.Request.Context()) + if err != nil { + logger.Errorw("[PanDomainMiddleware] Query client list failed", logger.Field("error", err.Error())) + } + for _, item := range clients { + u := strings.ToLower(item.UserAgent) + u = strings.Trim(u, " ") + browserKeywords = append(browserKeywords, u) + } + + for _, keyword := range browserKeywords { + keyword = strings.ToLower(strings.Trim(keyword, " ")) + if keyword == "" { + continue + } + if strings.Contains(strings.ToLower(ua), keyword) { + allow = true + } + } + if !allow { + c.String(http.StatusForbidden, "Access denied") + c.Abort() + return + } + } + domain := c.Request.Host domainArr := strings.Split(domain, ".") domainFirst := domainArr[0] @@ -21,7 +64,7 @@ func PanDomainMiddleware(svc *svc.ServiceContext) func(c *gin.Context) { UA: c.Request.Header.Get("User-Agent"), } l := subscribe.NewSubscribeLogic(c, svc) - resp, err := l.Generate(&request) + resp, err := l.Handler(&request) if err != nil { return } diff --git a/internal/middleware/traceMiddleware.go b/internal/middleware/traceMiddleware.go index a697cb5..6e143a8 100644 --- a/internal/middleware/traceMiddleware.go +++ b/internal/middleware/traceMiddleware.go @@ -9,14 +9,12 @@ import ( "github.com/perfect-panel/server/pkg/constant" "github.com/gin-gonic/gin" - "github.com/google/uuid" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" semconv "go.opentelemetry.io/otel/semconv/v1.24.0" oteltrace "go.opentelemetry.io/otel/trace" "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/pkg/logger" "github.com/perfect-panel/server/pkg/trace" ) @@ -71,19 +69,13 @@ func TraceMiddleware(_ *svc.ServiceContext) func(ctx *gin.Context) { ) defer span.End() - requestId, err := uuid.NewV7() - if err != nil { - logger.Errorw( - "failed to generate request id in uuid v7 format, fallback to uuid v4", - logger.Field("error", err), - ) - requestId = uuid.New() - } - c.Header(trace.RequestIdKey, requestId.String()) + requestId := trace.TraceIDFromContext(ctx) + + c.Header(trace.RequestIdKey, requestId) span.SetAttributes(requestAttributes(c.Request)...) span.SetAttributes( - attribute.String("http.request_id", requestId.String()), + attribute.String("http.request_id", requestId), semconv.HTTPRouteKey.String(c.FullPath()), ) // context with request host diff --git a/internal/model/announcement/model.go b/internal/model/announcement/model.go index f5575a8..973fc97 100644 --- a/internal/model/announcement/model.go +++ b/internal/model/announcement/model.go @@ -43,7 +43,7 @@ func (m *customAnnouncementModel) GetAnnouncementListByPage(ctx context.Context, if filter.Search != "" { conn = conn.Where("`title` LIKE ? OR `content` LIKE ?", "%"+filter.Search+"%", "%"+filter.Search+"%") } - return conn.Count(&total).Offset((page - 1) * size).Limit(size).Find(v).Error + return conn.Count(&total).Offset((page - 1) * size).Limit(size).Find(&list).Error }) return total, list, err } diff --git a/internal/model/application/application.go b/internal/model/application/application.go deleted file mode 100644 index bc6471f..0000000 --- a/internal/model/application/application.go +++ /dev/null @@ -1,54 +0,0 @@ -package application - -import ( - "time" -) - -type Application struct { - Id int64 `gorm:"primary_key"` - Name string `gorm:"type:varchar(255);default:'';not null;comment:应用名称"` - Icon string `gorm:"type:text;not null;comment:应用图标"` - Description string `gorm:"type:text;comment:更新描述"` - SubscribeType string `gorm:"type:varchar(50);default:'';not null;comment:订阅类型"` - ApplicationVersions []ApplicationVersion - CreatedAt time.Time `gorm:"<-:create;comment:创建时间"` - UpdatedAt time.Time `gorm:"comment:更新时间"` -} - -func (Application) TableName() string { - return "application" -} - -type ApplicationVersion struct { - Id int64 `gorm:"primary_key"` - Url string `gorm:"type:varchar(255);default:'';not null;comment:应用地址"` - Version string `gorm:"type:varchar(255);default:'';not null;comment:应用版本"` - Platform string `gorm:"type:varchar(50);default:'';not null;comment:应用平台"` - IsDefault bool `gorm:"type:tinyint(1);not null;default:0;comment:默认版本"` - Description string `gorm:"type:text;comment:更新描述"` - ApplicationId int64 `gorm:"comment:所属应用"` - CreatedAt time.Time `gorm:"<-:create;comment:创建时间"` - UpdatedAt time.Time `gorm:"comment:更新时间"` -} - -func (ApplicationVersion) TableName() string { - return "application_version" -} - -type ApplicationConfig struct { - Id int64 `gorm:"primary_key"` - AppId int64 `gorm:"type:int;not null;default:0;comment:App id"` - EncryptionKey string `gorm:"type:text;comment:Encryption Key"` - EncryptionMethod string `gorm:"type:varchar(255);comment:Encryption Method"` - Domains string `gorm:"type:text;comment:Domains"` - StartupPicture string `gorm:"type:text;comment:Startup Picture"` - StartupPictureSkipTime int64 `gorm:"type:int;not null;default:0;comment:Startup Picture Skip Time"` - InvitationLink string `gorm:"type:text;comment:Invitation Link"` - KrWebsiteId string `gorm:"type:varchar(255);default:'';comment:Kr Website ID"` - CreatedAt time.Time `gorm:"<-:create;comment:Create Time"` - UpdatedAt time.Time `gorm:"comment:Update Time"` -} - -func (ApplicationConfig) TableName() string { - return "application_config" -} diff --git a/internal/model/application/default.go b/internal/model/application/default.go deleted file mode 100644 index 8245827..0000000 --- a/internal/model/application/default.go +++ /dev/null @@ -1,245 +0,0 @@ -package application - -import ( - "context" - "errors" - "fmt" - - "github.com/perfect-panel/server/internal/config" - "github.com/perfect-panel/server/pkg/cache" - "github.com/redis/go-redis/v9" - "gorm.io/gorm" -) - -var _ Model = (*customApplicationModel)(nil) -var ( - cacheApplicationIdPrefix = "cache:application:id:" - cacheApplicationConfigIdPrefix = "cache:application:config:id:" - cacheApplicationVersionIdPrefix = "cache:application:version:id:" -) - -type ( - Model interface { - applicationModel - customApplicationLogicModel - } - applicationModel interface { - Insert(ctx context.Context, data *Application) error - FindOne(ctx context.Context, id int64) (*Application, error) - Update(ctx context.Context, data *Application) error - Delete(ctx context.Context, id int64) error - InsertVersion(ctx context.Context, data *ApplicationVersion) error - FindOneVersion(ctx context.Context, id int64) (*ApplicationVersion, error) - UpdateVersion(ctx context.Context, data *ApplicationVersion) error - InsertConfig(ctx context.Context, data *ApplicationConfig) error - FindOneConfig(ctx context.Context, id int64) (*ApplicationConfig, error) - UpdateConfig(ctx context.Context, data *ApplicationConfig) error - DeleteVersion(ctx context.Context, id int64) error - Transaction(ctx context.Context, fn func(db *gorm.DB) error) error - } - - customApplicationModel struct { - *defaultApplicationModel - } - defaultApplicationModel struct { - cache.CachedConn - table string - } -) - -func newApplicationModel(db *gorm.DB, c *redis.Client) *defaultApplicationModel { - return &defaultApplicationModel{ - CachedConn: cache.NewConn(db, c), - table: "`Application`", - } -} - -func (m *defaultApplicationModel) getCacheKeys(data *Application) []string { - if data == nil { - return []string{} - } - ApplicationIdKey := fmt.Sprintf("%s%v", cacheApplicationIdPrefix, data.Id) - cacheKeys := []string{ - ApplicationIdKey, - config.ApplicationKey, - } - return cacheKeys -} - -func (m *defaultApplicationModel) Insert(ctx context.Context, data *Application) error { - err := m.ExecCtx(ctx, func(conn *gorm.DB) error { - return conn.Create(&data).Error - }, m.getCacheKeys(data)...) - return err -} - -func (m *defaultApplicationModel) FindOne(ctx context.Context, id int64) (*Application, error) { - ApplicationIdKey := fmt.Sprintf("%s%v", cacheApplicationIdPrefix, id) - var resp Application - err := m.QueryCtx(ctx, &resp, ApplicationIdKey, func(conn *gorm.DB, v interface{}) error { - return conn.Model(&Application{}).Preload("ApplicationVersions").Where("`id` = ?", id).First(&resp).Error - }) - switch { - case err == nil: - return &resp, nil - default: - return nil, err - } -} - -func (m *defaultApplicationModel) Update(ctx context.Context, data *Application) error { - old, err := m.FindOne(ctx, data.Id) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return err - } - err = m.ExecCtx(ctx, func(conn *gorm.DB) error { - db := conn - return db.Save(data).Error - }, m.getCacheKeys(old)...) - return err -} - -func (m *defaultApplicationModel) Delete(ctx context.Context, id int64) error { - data, err := m.FindOne(ctx, id) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil - } - return err - } - err = m.ExecCtx(ctx, func(conn *gorm.DB) error { - db := conn - err = db.Where("application_id = ?", id).Delete(&ApplicationVersion{}).Error - if err != nil { - return err - } - return db.Delete(&Application{}, id).Error - }, m.getCacheKeys(data)...) - return err -} - -func (m *defaultApplicationModel) getVersionCacheKeys(data *ApplicationVersion) []string { - if data == nil { - return []string{} - } - ApplicationVersionIdKey := fmt.Sprintf("%s%v", cacheApplicationVersionIdPrefix, data.Id) - cacheKeys := []string{ - ApplicationVersionIdKey, - config.ApplicationKey, - } - return cacheKeys -} -func (m *defaultApplicationModel) getConfigCacheKeys(data *ApplicationConfig) []string { - if data == nil { - return []string{} - } - ApplicationConfigIdKey := fmt.Sprintf("%s%v", cacheApplicationConfigIdPrefix, data.Id) - cacheKeys := []string{ - ApplicationConfigIdKey, - config.ApplicationKey, - } - return cacheKeys -} - -func (m *defaultApplicationModel) InsertVersion(ctx context.Context, data *ApplicationVersion) error { - err := m.ExecCtx(ctx, func(conn *gorm.DB) error { - return conn.Transaction(func(tx *gorm.DB) error { - if data.IsDefault { - err := tx.Model(&ApplicationVersion{}). - Where("application_id = ? and platform = ? and default_version = ?", data.ApplicationId, data.Platform, data.IsDefault). - Updates(map[string]interface{}{"default_version": false}).Error - if err != nil { - return err - } - } - return tx.Create(&data).Error - }) - }, m.getVersionCacheKeys(data)...) - return err -} - -func (m *defaultApplicationModel) FindOneVersion(ctx context.Context, id int64) (*ApplicationVersion, error) { - ApplicationVersionIdKey := fmt.Sprintf("%s%v", cacheApplicationVersionIdPrefix, id) - var resp ApplicationVersion - err := m.QueryCtx(ctx, &resp, ApplicationVersionIdKey, func(conn *gorm.DB, v interface{}) error { - return conn.Model(&ApplicationVersion{}).Where("`id` = ?", id).First(&resp).Error - }) - switch { - case err == nil: - return &resp, nil - default: - return nil, err - } -} - -func (m *defaultApplicationModel) UpdateVersion(ctx context.Context, data *ApplicationVersion) error { - old, err := m.FindOneVersion(ctx, data.Id) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return err - } - err = m.ExecCtx(ctx, func(conn *gorm.DB) error { - return conn.Transaction(func(tx *gorm.DB) error { - if data.IsDefault { - err := tx.Model(&ApplicationVersion{}). - Where("application_id = ? and platform = ? and default_version = ?", data.ApplicationId, data.Platform, data.IsDefault). - Updates(map[string]interface{}{"default_version": false}).Error - if err != nil { - return err - } - } - return tx.Save(data).Error - }) - }, m.getVersionCacheKeys(old)...) - return err -} - -func (m *defaultApplicationModel) InsertConfig(ctx context.Context, data *ApplicationConfig) error { - err := m.ExecCtx(ctx, func(conn *gorm.DB) error { - return conn.Create(&data).Error - }, m.getConfigCacheKeys(data)...) - return err -} - -func (m *defaultApplicationModel) FindOneConfig(ctx context.Context, id int64) (*ApplicationConfig, error) { - ApplicationConfigIdKey := fmt.Sprintf("%s%v", cacheApplicationConfigIdPrefix, id) - var resp ApplicationConfig - err := m.QueryCtx(ctx, &resp, ApplicationConfigIdKey, func(conn *gorm.DB, v interface{}) error { - return conn.Model(&ApplicationConfig{}).Where("`id` = ?", id).First(&resp).Error - }) - switch { - case err == nil: - return &resp, nil - default: - return nil, err - } -} - -func (m *defaultApplicationModel) UpdateConfig(ctx context.Context, data *ApplicationConfig) error { - old, err := m.FindOneConfig(ctx, data.Id) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return err - } - err = m.ExecCtx(ctx, func(conn *gorm.DB) error { - return conn.Save(data).Error - }, m.getConfigCacheKeys(old)...) - return err -} - -func (m *defaultApplicationModel) DeleteVersion(ctx context.Context, id int64) error { - data, err := m.FindOneVersion(ctx, id) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil - } - return err - } - err = m.ExecCtx(ctx, func(conn *gorm.DB) error { - db := conn - return db.Delete(&ApplicationVersion{}, id).Error - }, m.getVersionCacheKeys(data)...) - return err -} - -func (m *defaultApplicationModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error { - return m.TransactCtx(ctx, fn) -} diff --git a/internal/model/application/model.go b/internal/model/application/model.go deleted file mode 100644 index 3bd5b0a..0000000 --- a/internal/model/application/model.go +++ /dev/null @@ -1,16 +0,0 @@ -package application - -import ( - "github.com/redis/go-redis/v9" - "gorm.io/gorm" -) - -type customApplicationLogicModel interface { -} - -// NewModel returns a model for the database table. -func NewModel(conn *gorm.DB, c *redis.Client) Model { - return &customApplicationModel{ - defaultApplicationModel: newApplicationModel(conn, c), - } -} diff --git a/internal/model/cache/constant.go b/internal/model/cache/constant.go deleted file mode 100644 index 5ae995c..0000000 --- a/internal/model/cache/constant.go +++ /dev/null @@ -1,52 +0,0 @@ -package cache - -const ( - // UserTodayUploadTrafficCacheKey 用户当日上传流量 - UserTodayUploadTrafficCacheKey = "node:user_today_upload_traffic" - // UserTodayDownloadTrafficCacheKey 用户当日下载流量 - UserTodayDownloadTrafficCacheKey = "node:user_today_download_traffic" - // UserTodayTotalTrafficCacheKey 用户当日总流量 - UserTodayTotalTrafficCacheKey = "node:user_today_total_traffic" - // NodeTodayUploadTrafficCacheKey 节点当日上传流量 - NodeTodayUploadTrafficCacheKey = "node:node_today_upload_traffic" - // NodeTodayDownloadTrafficCacheKey 节点当日下载流量 - NodeTodayDownloadTrafficCacheKey = "node:node_today_download_traffic" - // NodeTodayTotalTrafficCacheKey 节点当日总流量 - NodeTodayTotalTrafficCacheKey = "node:node_today_total_traffic" - // UserTodayUploadTrafficRankKey 用户当日上传流量排行榜 - UserTodayUploadTrafficRankKey = "node:user_today_upload_traffic_rank" - // UserTodayDownloadTrafficRankKey 用户当日下载流量排行榜 - UserTodayDownloadTrafficRankKey = "node:user_today_download_traffic_rank" - // UserTodayTotalTrafficRankKey 用户当日总流量排行榜 - UserTodayTotalTrafficRankKey = "node:user_today_total_traffic_rank" - // NodeTodayUploadTrafficRankKey 节点当日上传流量排行榜 - NodeTodayUploadTrafficRankKey = "node:node_today_upload_traffic_rank" - // NodeTodayDownloadTrafficRankKey 节点当日下载流量排行榜 - NodeTodayDownloadTrafficRankKey = "node:node_today_download_traffic_rank" - // NodeTodayTotalTrafficRankKey 节点当日总流量排行榜 - NodeTodayTotalTrafficRankKey = "node:node_today_total_traffic_rank" - // NodeOnlineUserCacheKey 节点在线用户 - NodeOnlineUserCacheKey = "node:node_online_user:%d" - // UserOnlineIpCacheKey 用户在线IP - UserOnlineIpCacheKey = "node:user_online_ip:%d" - // AllNodeOnlineUserCacheKey 所有节点在线用户 - AllNodeOnlineUserCacheKey = "node:all_node_online_user" - // NodeStatusCacheKey 节点状态 - NodeStatusCacheKey = "node:status:%d" - // AllNodeDownloadTrafficCacheKey 所有节点下载流量 - AllNodeDownloadTrafficCacheKey = "node:all_node_download_traffic" - // AllNodeUploadTrafficCacheKey 所有节点上传流量 - AllNodeUploadTrafficCacheKey = "node:all_node_upload_traffic" - // YesterdayTotalTrafficRank 昨日节点总流量排行榜 - YesterdayNodeTotalTrafficRank = "node:yesterday_total_traffic_rank" - // YesterdayUploadTrafficRank 昨日节点上传流量排行榜 - YesterdayNodeUploadTrafficRank = "node:yesterday_upload_traffic_rank" - // YesterdayDownloadTrafficRank 昨日节点下载流量排行榜 - YesterdayNodeDownloadTrafficRank = "node:yesterday_download_traffic_rank" - // YesterdayUserTotalTrafficRank 昨日用户总流量排行榜 - YesterdayUserTotalTrafficRank = "node:yesterday_user_total_traffic_rank" - // YesterdayUserUploadTrafficRank 昨日用户上传流量排行榜 - YesterdayUserUploadTrafficRank = "node:yesterday_user_upload_traffic_rank" - // YesterdayUserDownloadTrafficRank 昨日用户下载流量排行榜 - YesterdayUserDownloadTrafficRank = "node:yesterday_user_download_traffic_rank" -) diff --git a/internal/model/cache/node.go b/internal/model/cache/node.go deleted file mode 100644 index 9b65b15..0000000 --- a/internal/model/cache/node.go +++ /dev/null @@ -1,584 +0,0 @@ -package cache - -import ( - "context" - "encoding/json" - "fmt" - "strconv" - "sync" - "time" - - "github.com/perfect-panel/server/pkg/logger" - "github.com/redis/go-redis/v9" -) - -type NodeCacheClient struct { - *redis.Client - resetMutex sync.Mutex -} - -func NewNodeCacheClient(rds *redis.Client) *NodeCacheClient { - return &NodeCacheClient{ - Client: rds, - } -} - -// AddOnlineUserIP adds user's online IP -func (c *NodeCacheClient) AddOnlineUserIP(ctx context.Context, users []NodeOnlineUser) error { - if len(users) == 0 { - // No users to add - return nil - } - - // Use Pipeline to optimize Redis operations - pipe := c.Pipeline() - - // Add user online IPs and clean up expired IPs for each user - for _, user := range users { - if user.SID <= 0 || user.IP == "" { - logger.Errorf("invalid user data: uid=%d, ip=%s", user.SID, user.IP) - continue - } - - key := fmt.Sprintf(UserOnlineIpCacheKey, user.SID) - now := time.Now() - expireTime := now.Add(5 * time.Minute) - - // Clean up expired user online IPs - pipe.ZRemRangeByScore(ctx, key, "0", fmt.Sprintf("%d", now.Unix())) - pipe.ZRemRangeByScore(ctx, AllNodeOnlineUserCacheKey, "0", fmt.Sprintf("%d", now.Unix())) - - // Add or update user online IP - // XX: Only update elements that already exist - // NX: Only add new elements - _ = pipe.ZAdd(ctx, key, redis.Z{ - Score: float64(expireTime.Unix()), - Member: user.IP, - }).Err() - _ = pipe.ZAdd(ctx, AllNodeOnlineUserCacheKey, redis.Z{ - Score: float64(expireTime.Unix()), - Member: user.IP, - }).Err() - - // Set key expiration to 5 minutes (slightly longer than IP expiration) - pipe.Expire(ctx, key, 5*time.Minute) - pipe.Expire(ctx, AllNodeOnlineUserCacheKey, 5*time.Minute) - } - - // Execute all commands - _, err := pipe.Exec(ctx) - if err != nil { - return fmt.Errorf("failed to add node user online ip: %w", err) - } - return nil -} - -// GetUserOnlineIp gets user's online IPs -func (c *NodeCacheClient) GetUserOnlineIp(ctx context.Context, uid int64) ([]string, error) { - if uid <= 0 { - return nil, fmt.Errorf("invalid parameters: uid=%d", uid) - } - - // Get user's online IPs - ips, err := c.ZRevRangeByScore(ctx, fmt.Sprintf(UserOnlineIpCacheKey, uid), &redis.ZRangeBy{ - Min: "0", - Max: fmt.Sprintf("%d", time.Now().Add(5*time.Minute).Unix()), - Offset: 0, - Count: 100, - }).Result() - if err != nil { - return nil, fmt.Errorf("failed to get user online ip: %w", err) - } - return ips, nil -} - -// UpdateNodeOnlineUser updates node's online users and IPs -func (c *NodeCacheClient) UpdateNodeOnlineUser(ctx context.Context, nodeId int64, users []NodeOnlineUser) error { - if nodeId <= 0 || len(users) == 0 { - return fmt.Errorf("invalid parameters: nodeId=%d, users=%v", nodeId, users) - } - // Organize data - data := make(map[int64][]string) - for _, user := range users { - data[user.SID] = append(data[user.SID], user.IP) - } - - value, err := json.Marshal(data) - if err != nil { - return fmt.Errorf("failed to marshal data: %w", err) - } - - c.Set(ctx, fmt.Sprintf(NodeOnlineUserCacheKey, nodeId), value, time.Minute*5) - return nil -} - -// GetNodeOnlineUser gets node's online users and IPs -func (c *NodeCacheClient) GetNodeOnlineUser(ctx context.Context, nodeId int64) (map[int64][]string, error) { - if nodeId <= 0 { - return nil, fmt.Errorf("invalid parameters: nodeId=%d", nodeId) - } - value, err := c.Get(ctx, fmt.Sprintf(NodeOnlineUserCacheKey, nodeId)).Result() - if err != nil { - return nil, fmt.Errorf("failed to get node online user: %w", err) - } - var data map[int64][]string - if err := json.Unmarshal([]byte(value), &data); err != nil { - return nil, fmt.Errorf("failed to unmarshal data: %w", err) - } - return data, nil -} - -// AddUserTodayTraffic Add user's today traffic -func (c *NodeCacheClient) AddUserTodayTraffic(ctx context.Context, uid int64, upload, download int64) error { - if uid <= 0 || upload <= 0 { - return fmt.Errorf("invalid parameters: uid=%d, upload=%d", uid, upload) - } - pipe := c.Pipeline() - // User's today upload traffic - pipe.HIncrBy(ctx, UserTodayUploadTrafficCacheKey, fmt.Sprintf("%d", uid), upload) - // User's today download traffic - pipe.HIncrBy(ctx, UserTodayDownloadTrafficCacheKey, fmt.Sprintf("%d", uid), download) - // User's today total traffic - pipe.HIncrBy(ctx, UserTodayTotalTrafficCacheKey, fmt.Sprintf("%d", uid), upload+download) - // User's today traffic ranking - pipe.ZIncrBy(ctx, UserTodayUploadTrafficRankKey, float64(upload), fmt.Sprintf("%d", uid)) - pipe.ZIncrBy(ctx, UserTodayDownloadTrafficRankKey, float64(download), fmt.Sprintf("%d", uid)) - pipe.ZIncrBy(ctx, UserTodayTotalTrafficRankKey, float64(upload+download), fmt.Sprintf("%d", uid)) - - // All node upload traffic - pipe.IncrBy(ctx, AllNodeUploadTrafficCacheKey, upload) - // All node download traffic - pipe.IncrBy(ctx, AllNodeDownloadTrafficCacheKey, download) - // Execute commands - _, err := pipe.Exec(ctx) - if err != nil { - return fmt.Errorf("failed to add user today upload traffic: %w", err) - } - return nil -} - -// AddNodeTodayTraffic Add node's today traffic -func (c *NodeCacheClient) AddNodeTodayTraffic(ctx context.Context, nodeId int64, userTraffic []UserTraffic) error { - if nodeId <= 0 || len(userTraffic) == 0 { - return fmt.Errorf("invalid parameters: nodeId=%d, userTraffic=%v", nodeId, userTraffic) - } - pipe := c.Pipeline() - upload, download, total := c.calculateTraffic(userTraffic) - pipe.HIncrBy(ctx, NodeTodayUploadTrafficCacheKey, fmt.Sprintf("%d", nodeId), upload) - pipe.HIncrBy(ctx, NodeTodayDownloadTrafficCacheKey, fmt.Sprintf("%d", nodeId), download) - pipe.HIncrBy(ctx, NodeTodayTotalTrafficCacheKey, fmt.Sprintf("%d", nodeId), total) - pipe.ZIncrBy(ctx, NodeTodayUploadTrafficRankKey, float64(upload), fmt.Sprintf("%d", nodeId)) - pipe.ZIncrBy(ctx, NodeTodayDownloadTrafficRankKey, float64(download), fmt.Sprintf("%d", nodeId)) - pipe.ZIncrBy(ctx, NodeTodayTotalTrafficRankKey, float64(total), fmt.Sprintf("%d", nodeId)) - // Execute commands - _, err := pipe.Exec(ctx) - if err != nil { - return fmt.Errorf("failed to add node today upload traffic: %w", err) - } - return nil -} - -// Get user's traffic data -func (c *NodeCacheClient) getUserTrafficData(ctx context.Context, uid int64) (upload, download int64, err error) { - upload, err = c.HGet(ctx, UserTodayUploadTrafficCacheKey, fmt.Sprintf("%d", uid)).Int64() - if err != nil { - return 0, 0, fmt.Errorf("failed to get user today upload traffic: %w", err) - } - download, err = c.HGet(ctx, UserTodayDownloadTrafficCacheKey, fmt.Sprintf("%d", uid)).Int64() - if err != nil { - return 0, 0, fmt.Errorf("failed to get user today download traffic: %w", err) - } - return upload, download, nil -} - -// Get node's traffic data -func (c *NodeCacheClient) getNodeTrafficData(ctx context.Context, nodeId int64) (upload, download int64, err error) { - upload, err = c.HGet(ctx, NodeTodayUploadTrafficCacheKey, fmt.Sprintf("%d", nodeId)).Int64() - if err != nil { - return 0, 0, fmt.Errorf("failed to get node today upload traffic: %w", err) - } - download, err = c.HGet(ctx, NodeTodayDownloadTrafficCacheKey, fmt.Sprintf("%d", nodeId)).Int64() - if err != nil { - return 0, 0, fmt.Errorf("failed to get node today download traffic: %w", err) - } - return upload, download, nil -} - -// Parse ID -func (c *NodeCacheClient) parseID(member interface{}, idType string) (int64, error) { - id, err := strconv.ParseInt(member.(string), 10, 64) - if err != nil { - return 0, fmt.Errorf("failed to parse %s id %v: %w", idType, member, err) - } - return id, nil -} - -// GetUserTodayTotalTrafficRank Get user's today total traffic ranking top N -func (c *NodeCacheClient) GetUserTodayTotalTrafficRank(ctx context.Context, n int64) ([]UserTodayTrafficRank, error) { - if n <= 0 { - return nil, fmt.Errorf("invalid parameters: n=%d", n) - } - data, err := c.ZRevRangeWithScores(ctx, UserTodayTotalTrafficRankKey, 0, n-1).Result() - if err != nil { - return nil, fmt.Errorf("failed to get user today total traffic rank: %w", err) - } - users := make([]UserTodayTrafficRank, 0, len(data)) - for _, user := range data { - uid, err := c.parseID(user.Member, "user") - if err != nil { - logger.Errorf("%v", err) - continue - } - upload, download, err := c.getUserTrafficData(ctx, uid) - if err != nil { - logger.Errorf("%v", err) - continue - } - users = append(users, UserTodayTrafficRank{ - SID: uid, - Upload: upload, - Download: download, - Total: int64(user.Score), - }) - } - return users, nil -} - -// GetNodeTodayTotalTrafficRank Get node's today total traffic ranking top N -func (c *NodeCacheClient) GetNodeTodayTotalTrafficRank(ctx context.Context, n int64) ([]NodeTodayTrafficRank, error) { - if n <= 0 { - return nil, fmt.Errorf("invalid parameters: n=%d", n) - } - data, err := c.ZRevRangeWithScores(ctx, NodeTodayTotalTrafficRankKey, 0, n-1).Result() - if err != nil { - return nil, fmt.Errorf("failed to get node today total traffic rank: %w", err) - } - nodes := make([]NodeTodayTrafficRank, 0, len(data)) - for _, node := range data { - nodeId, err := c.parseID(node.Member, "node") - if err != nil { - logger.Errorf("%v", err) - continue - } - upload, download, err := c.getNodeTrafficData(ctx, nodeId) - if err != nil { - logger.Errorf("%v", err) - continue - } - nodes = append(nodes, NodeTodayTrafficRank{ - ID: nodeId, - Upload: upload, - Download: download, - Total: int64(node.Score), - }) - } - return nodes, nil -} - -// GetUserTodayUploadTrafficRank Get user's today upload traffic ranking top N -func (c *NodeCacheClient) GetUserTodayUploadTrafficRank(ctx context.Context, n int64) ([]UserTodayTrafficRank, error) { - if n <= 0 { - return nil, fmt.Errorf("invalid parameters: n=%d", n) - } - data, err := c.ZRevRangeWithScores(ctx, UserTodayUploadTrafficRankKey, 0, n-1).Result() - if err != nil { - return nil, fmt.Errorf("failed to get user today upload traffic rank: %w", err) - } - users := make([]UserTodayTrafficRank, 0, len(data)) - for _, user := range data { - uid, err := c.parseID(user.Member, "user") - if err != nil { - logger.Errorf("%v", err) - continue - } - upload, download, err := c.getUserTrafficData(ctx, uid) - if err != nil { - logger.Errorf("%v", err) - continue - } - users = append(users, UserTodayTrafficRank{ - SID: uid, - Upload: upload, - Download: download, - Total: int64(user.Score), - }) - } - return users, nil -} - -// GetUserTodayDownloadTrafficRank Get user's today download traffic ranking top N -func (c *NodeCacheClient) GetUserTodayDownloadTrafficRank(ctx context.Context, n int64) ([]UserTodayTrafficRank, error) { - if n <= 0 { - return nil, fmt.Errorf("invalid parameters: n=%d", n) - } - data, err := c.ZRevRangeWithScores(ctx, UserTodayDownloadTrafficRankKey, 0, n-1).Result() - if err != nil { - return nil, fmt.Errorf("failed to get user today download traffic rank: %w", err) - } - users := make([]UserTodayTrafficRank, 0, len(data)) - for _, user := range data { - uid, err := c.parseID(user.Member, "user") - if err != nil { - logger.Errorf("%v", err) - continue - } - upload, download, err := c.getUserTrafficData(ctx, uid) - if err != nil { - logger.Errorf("%v", err) - continue - } - users = append(users, UserTodayTrafficRank{ - SID: uid, - Upload: upload, - Download: download, - Total: int64(user.Score), - }) - } - return users, nil -} - -// GetNodeTodayUploadTrafficRank Get node's today upload traffic ranking top N -func (c *NodeCacheClient) GetNodeTodayUploadTrafficRank(ctx context.Context, n int64) ([]NodeTodayTrafficRank, error) { - if n <= 0 { - return nil, fmt.Errorf("invalid parameters: n=%d", n) - } - data, err := c.ZRevRangeWithScores(ctx, NodeTodayUploadTrafficRankKey, 0, n-1).Result() - if err != nil { - return nil, fmt.Errorf("failed to get node today upload traffic rank: %w", err) - } - nodes := make([]NodeTodayTrafficRank, 0, len(data)) - for _, node := range data { - nodeId, err := c.parseID(node.Member, "node") - if err != nil { - logger.Errorf("%v", err) - continue - } - upload, download, err := c.getNodeTrafficData(ctx, nodeId) - if err != nil { - logger.Errorf("%v", err) - continue - } - nodes = append(nodes, NodeTodayTrafficRank{ - ID: nodeId, - Upload: upload, - Download: download, - Total: int64(node.Score), - }) - } - return nodes, nil -} - -// GetNodeTodayDownloadTrafficRank Get node's today download traffic ranking top N -func (c *NodeCacheClient) GetNodeTodayDownloadTrafficRank(ctx context.Context, n int64) ([]NodeTodayTrafficRank, error) { - if n <= 0 { - return nil, fmt.Errorf("invalid parameters: n=%d", n) - } - data, err := c.ZRevRangeWithScores(ctx, NodeTodayDownloadTrafficRankKey, 0, n-1).Result() - if err != nil { - return nil, fmt.Errorf("failed to get node today download traffic rank: %w", err) - } - nodes := make([]NodeTodayTrafficRank, 0, len(data)) - for _, node := range data { - nodeId, err := c.parseID(node.Member, "node") - if err != nil { - logger.Errorf("%v", err) - continue - } - upload, download, err := c.getNodeTrafficData(ctx, nodeId) - if err != nil { - logger.Errorf("%v", err) - continue - } - nodes = append(nodes, NodeTodayTrafficRank{ - ID: nodeId, - Upload: upload, - Download: download, - Total: int64(node.Score), - }) - } - return nodes, nil -} - -// ResetTodayTrafficData Reset today's traffic data -func (c *NodeCacheClient) ResetTodayTrafficData(ctx context.Context) error { - c.resetMutex.Lock() - defer c.resetMutex.Unlock() - pipe := c.Pipeline() - pipe.Del(ctx, UserTodayUploadTrafficCacheKey) - pipe.Del(ctx, UserTodayDownloadTrafficCacheKey) - pipe.Del(ctx, UserTodayTotalTrafficCacheKey) - pipe.Del(ctx, NodeTodayUploadTrafficCacheKey) - pipe.Del(ctx, NodeTodayDownloadTrafficCacheKey) - pipe.Del(ctx, NodeTodayTotalTrafficCacheKey) - pipe.Del(ctx, UserTodayUploadTrafficRankKey) - pipe.Del(ctx, UserTodayDownloadTrafficRankKey) - pipe.Del(ctx, UserTodayTotalTrafficRankKey) - pipe.Del(ctx, NodeTodayUploadTrafficRankKey) - pipe.Del(ctx, NodeTodayDownloadTrafficRankKey) - pipe.Del(ctx, NodeTodayTotalTrafficRankKey) - pipe.Del(ctx, AllNodeDownloadTrafficCacheKey) - pipe.Del(ctx, AllNodeUploadTrafficCacheKey) - _, err := pipe.Exec(ctx) - if err != nil { - return fmt.Errorf("failed to reset today traffic data: %w", err) - } - return nil -} - -// Calculate traffic -func (c *NodeCacheClient) calculateTraffic(data []UserTraffic) (upload, download, total int64) { - for _, userTraffic := range data { - upload += userTraffic.Upload - download += userTraffic.Download - total += userTraffic.Upload + userTraffic.Download - } - return upload, download, total -} - -// GetAllNodeOnlineUser Get all node online user -func (c *NodeCacheClient) GetAllNodeOnlineUser(ctx context.Context) ([]string, error) { - users, err := c.ZRevRange(ctx, AllNodeOnlineUserCacheKey, 0, -1).Result() - if err != nil { - return nil, fmt.Errorf("failed to get all node online user: %w", err) - } - return users, nil -} - -// UpdateNodeStatus Update node status -func (c *NodeCacheClient) UpdateNodeStatus(ctx context.Context, nodeId int64, status NodeStatus) error { - // 参数验证 - if nodeId <= 0 { - return fmt.Errorf("invalid node id: %d", nodeId) - } - - // 验证状态数据 - if status.UpdatedAt <= 0 { - return fmt.Errorf("invalid status data: updated_at=%d", status.UpdatedAt) - } - - // 序列化状态数据 - value, err := json.Marshal(status) - if err != nil { - return fmt.Errorf("failed to marshal node status: %w", err) - } - - // 使用 Pipeline 优化性能 - pipe := c.Pipeline() - - // 设置状态数据 - pipe.Set(ctx, fmt.Sprintf(NodeStatusCacheKey, nodeId), value, time.Minute*5) - - // 执行命令 - _, err = pipe.Exec(ctx) - if err != nil { - return fmt.Errorf("failed to update node status: %w", err) - } - - return nil -} - -// GetNodeStatus Get node status -func (c *NodeCacheClient) GetNodeStatus(ctx context.Context, nodeId int64) (NodeStatus, error) { - status, err := c.Get(ctx, fmt.Sprintf(NodeStatusCacheKey, nodeId)).Result() - if err != nil { - return NodeStatus{}, fmt.Errorf("failed to get node status: %w", err) - } - var nodeStatus NodeStatus - if err := json.Unmarshal([]byte(status), &nodeStatus); err != nil { - return NodeStatus{}, fmt.Errorf("failed to unmarshal node status: %w", err) - } - return nodeStatus, nil -} - -// GetOnlineNodeStatusCount Get Online Node Status Count -func (c *NodeCacheClient) GetOnlineNodeStatusCount(ctx context.Context) (int64, error) { - // 获取所有节点Key - keys, err := c.Keys(ctx, "node:status:*").Result() - if err != nil { - return 0, fmt.Errorf("failed to get all node status keys: %w", err) - } - var count int64 - for _, key := range keys { - status, err := c.Get(ctx, key).Result() - if err != nil { - logger.Errorf("failed to get node status: %v", err.Error()) - continue - } - if status != "" { - count++ - } - } - return count, nil -} - -// GetAllNodeUploadTraffic Get all node upload traffic -func (c *NodeCacheClient) GetAllNodeUploadTraffic(ctx context.Context) (int64, error) { - upload, err := c.Get(ctx, AllNodeUploadTrafficCacheKey).Int64() - if err != nil { - return 0, fmt.Errorf("failed to get all node upload traffic: %w", err) - } - return upload, nil -} - -// GetAllNodeDownloadTraffic Get all node download traffic -func (c *NodeCacheClient) GetAllNodeDownloadTraffic(ctx context.Context) (int64, error) { - download, err := c.Get(ctx, AllNodeDownloadTrafficCacheKey).Int64() - if err != nil { - return 0, fmt.Errorf("failed to get all node download traffic: %w", err) - } - return download, nil -} - -// UpdateYesterdayNodeTotalTrafficRank Update yesterday node total traffic rank -func (c *NodeCacheClient) UpdateYesterdayNodeTotalTrafficRank(ctx context.Context, nodes []NodeTodayTrafficRank) error { - expireAt := time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.Local).Add(time.Hour * 24) - t := time.Until(expireAt) - pipe := c.Pipeline() - value, _ := json.Marshal(nodes) - pipe.Set(ctx, YesterdayNodeTotalTrafficRank, value, t) - _, err := pipe.Exec(ctx) - if err != nil { - return fmt.Errorf("failed to update yesterday node total traffic rank: %w", err) - } - return nil -} - -// UpdateYesterdayUserTotalTrafficRank Update yesterday user total traffic rank -func (c *NodeCacheClient) UpdateYesterdayUserTotalTrafficRank(ctx context.Context, users []UserTodayTrafficRank) error { - expireAt := time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.Local).Add(time.Hour * 24) - t := time.Until(expireAt) - pipe := c.Pipeline() - value, _ := json.Marshal(users) - pipe.Set(ctx, YesterdayUserTotalTrafficRank, value, t) - _, err := pipe.Exec(ctx) - if err != nil { - return fmt.Errorf("failed to update yesterday user total traffic rank: %w", err) - } - return nil -} - -// GetYesterdayNodeTotalTrafficRank Get yesterday node total traffic rank -func (c *NodeCacheClient) GetYesterdayNodeTotalTrafficRank(ctx context.Context) ([]NodeTodayTrafficRank, error) { - value, err := c.Get(ctx, YesterdayNodeTotalTrafficRank).Result() - if err != nil { - return nil, fmt.Errorf("failed to get yesterday node total traffic rank: %w", err) - } - var nodes []NodeTodayTrafficRank - if err := json.Unmarshal([]byte(value), &nodes); err != nil { - return nil, fmt.Errorf("failed to unmarshal yesterday node total traffic rank: %w", err) - } - return nodes, nil -} - -// GetYesterdayUserTotalTrafficRank Get yesterday user total traffic rank -func (c *NodeCacheClient) GetYesterdayUserTotalTrafficRank(ctx context.Context) ([]UserTodayTrafficRank, error) { - value, err := c.Get(ctx, YesterdayUserTotalTrafficRank).Result() - if err != nil { - return nil, fmt.Errorf("failed to get yesterday user total traffic rank: %w", err) - } - var users []UserTodayTrafficRank - if err := json.Unmarshal([]byte(value), &users); err != nil { - return nil, fmt.Errorf("failed to unmarshal yesterday user total traffic rank: %w", err) - } - return users, nil -} diff --git a/internal/model/cache/node_test.go b/internal/model/cache/node_test.go deleted file mode 100644 index b7660ab..0000000 --- a/internal/model/cache/node_test.go +++ /dev/null @@ -1,575 +0,0 @@ -package cache - -import ( - "context" - "encoding/json" - "fmt" - "testing" - "time" - - "github.com/alicebob/miniredis/v2" - "github.com/redis/go-redis/v9" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// Create a test Redis client -func newTestRedisClient(t *testing.T) *redis.Client { - mr, err := miniredis.Run() - require.NoError(t, err) - - client := redis.NewClient(&redis.Options{ - Addr: mr.Addr(), - }) - require.NoError(t, client.Ping(context.Background()).Err()) - return client -} - -// Clean up test data -func cleanupTestData(t *testing.T, client *redis.Client) { - ctx := context.Background() - keys := []string{ - UserTodayUploadTrafficCacheKey, - UserTodayDownloadTrafficCacheKey, - UserTodayTotalTrafficCacheKey, - NodeTodayUploadTrafficCacheKey, - NodeTodayDownloadTrafficCacheKey, - NodeTodayTotalTrafficCacheKey, - UserTodayUploadTrafficRankKey, - UserTodayDownloadTrafficRankKey, - UserTodayTotalTrafficRankKey, - NodeTodayUploadTrafficRankKey, - NodeTodayDownloadTrafficRankKey, - NodeTodayTotalTrafficRankKey, - } - - // Clean up all cache keys - for _, key := range keys { - require.NoError(t, client.Del(ctx, key).Err()) - } - - // Clean up user online IP cache - for uid := int64(1); uid <= 3; uid++ { - require.NoError(t, client.Del(ctx, fmt.Sprintf(UserOnlineIpCacheKey, uid)).Err()) - } - - // Clean up node online user cache - for nodeId := int64(1); nodeId <= 3; nodeId++ { - require.NoError(t, client.Del(ctx, fmt.Sprintf(NodeOnlineUserCacheKey, nodeId)).Err()) - } -} - -func TestNodeCacheClient_AddUserTodayTraffic(t *testing.T) { - client := newTestRedisClient(t) - defer cleanupTestData(t, client) - - cache := NewNodeCacheClient(client) - ctx := context.Background() - - tests := []struct { - name string - uid int64 - upload int64 - download int64 - wantErr bool - }{ - { - name: "Add traffic normally", - uid: 1, - upload: 100, - download: 200, - wantErr: false, - }, - { - name: "Invalid SID", - uid: 0, - upload: 100, - download: 200, - wantErr: true, - }, - { - name: "Invalid upload traffic", - uid: 1, - upload: 0, - download: 200, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := cache.AddUserTodayTraffic(ctx, tt.uid, tt.upload, tt.download) - if tt.wantErr { - assert.Error(t, err) - return - } - assert.NoError(t, err) - - // Verify data is added correctly - upload, err := client.HGet(ctx, UserTodayUploadTrafficCacheKey, "1").Int64() - assert.NoError(t, err) - assert.Equal(t, tt.upload, upload) - - download, err := client.HGet(ctx, UserTodayDownloadTrafficCacheKey, "1").Int64() - assert.NoError(t, err) - assert.Equal(t, tt.download, download) - }) - } -} - -func TestNodeCacheClient_AddNodeTodayTraffic(t *testing.T) { - client := newTestRedisClient(t) - defer cleanupTestData(t, client) - - cache := NewNodeCacheClient(client) - ctx := context.Background() - - tests := []struct { - name string - nodeId int64 - userTraffic []UserTraffic - wantErr bool - }{ - { - name: "Add node traffic normally", - nodeId: 1, - userTraffic: []UserTraffic{ - {UID: 1, Upload: 100, Download: 200}, - {UID: 2, Upload: 300, Download: 400}, - }, - wantErr: false, - }, - { - name: "Invalid node ID", - nodeId: 0, - userTraffic: []UserTraffic{ - {UID: 1, Upload: 100, Download: 200}, - }, - wantErr: true, - }, - { - name: "Empty user traffic data", - nodeId: 1, - userTraffic: []UserTraffic{}, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := cache.AddNodeTodayTraffic(ctx, tt.nodeId, tt.userTraffic) - if tt.wantErr { - assert.Error(t, err) - return - } - assert.NoError(t, err) - - // Verify data is added correctly - upload, err := client.HGet(ctx, NodeTodayUploadTrafficCacheKey, "1").Int64() - assert.NoError(t, err) - assert.Equal(t, int64(400), upload) // 100 + 300 - - download, err := client.HGet(ctx, NodeTodayDownloadTrafficCacheKey, "1").Int64() - assert.NoError(t, err) - assert.Equal(t, int64(600), download) // 200 + 400 - }) - } -} - -func TestNodeCacheClient_GetUserTodayTrafficRank(t *testing.T) { - client := newTestRedisClient(t) - defer cleanupTestData(t, client) - - cache := NewNodeCacheClient(client) - ctx := context.Background() - - // Prepare test data - testData := []struct { - uid int64 - upload int64 - download int64 - }{ - {1, 100, 200}, - {2, 300, 400}, - {3, 500, 600}, - } - - for _, data := range testData { - err := cache.AddUserTodayTraffic(ctx, data.uid, data.upload, data.download) - require.NoError(t, err) - } - - tests := []struct { - name string - n int64 - wantErr bool - }{ - { - name: "Get top 2 ranks", - n: 2, - wantErr: false, - }, - { - name: "Get all ranks", - n: 3, - wantErr: false, - }, - { - name: "Invalid N value", - n: 0, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ranks, err := cache.GetUserTodayTotalTrafficRank(ctx, tt.n) - if tt.wantErr { - assert.Error(t, err) - return - } - assert.NoError(t, err) - assert.Len(t, ranks, int(tt.n)) - - // Verify sorting is correct - for i := 1; i < len(ranks); i++ { - assert.GreaterOrEqual(t, ranks[i-1].Total, ranks[i].Total) - } - }) - } -} - -func TestNodeCacheClient_ResetTodayTrafficData(t *testing.T) { - client := newTestRedisClient(t) - defer cleanupTestData(t, client) - - cache := NewNodeCacheClient(client) - ctx := context.Background() - - // Prepare test data - err := cache.AddUserTodayTraffic(ctx, 1, 100, 200) - require.NoError(t, err) - err = cache.AddNodeTodayTraffic(ctx, 1, []UserTraffic{{UID: 1, Upload: 100, Download: 200}}) - require.NoError(t, err) - - // Test reset functionality - err = cache.ResetTodayTrafficData(ctx) - assert.NoError(t, err) - - // Verify data is cleared - keys := []string{ - UserTodayUploadTrafficCacheKey, - UserTodayDownloadTrafficCacheKey, - UserTodayTotalTrafficCacheKey, - NodeTodayUploadTrafficCacheKey, - NodeTodayDownloadTrafficCacheKey, - NodeTodayTotalTrafficCacheKey, - } - - for _, key := range keys { - exists, err := client.Exists(ctx, key).Result() - assert.NoError(t, err) - assert.Equal(t, int64(0), exists) - } -} - -func TestNodeCacheClient_GetNodeTodayTrafficRank(t *testing.T) { - client := newTestRedisClient(t) - defer cleanupTestData(t, client) - - cache := NewNodeCacheClient(client) - ctx := context.Background() - - // Prepare test data - testData := []struct { - nodeId int64 - traffic []UserTraffic - }{ - {1, []UserTraffic{{UID: 1, Upload: 100, Download: 200}}}, - {2, []UserTraffic{{UID: 2, Upload: 300, Download: 400}}}, - {3, []UserTraffic{{UID: 3, Upload: 500, Download: 600}}}, - } - - for _, data := range testData { - err := cache.AddNodeTodayTraffic(ctx, data.nodeId, data.traffic) - require.NoError(t, err) - } - - tests := []struct { - name string - n int64 - wantErr bool - }{ - { - name: "Get top 2 ranks", - n: 2, - wantErr: false, - }, - { - name: "Get all ranks", - n: 3, - wantErr: false, - }, - { - name: "Invalid N value", - n: 0, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ranks, err := cache.GetNodeTodayTotalTrafficRank(ctx, tt.n) - if tt.wantErr { - assert.Error(t, err) - return - } - assert.NoError(t, err) - assert.Len(t, ranks, int(tt.n)) - - // Verify sorting is correct - for i := 1; i < len(ranks); i++ { - assert.GreaterOrEqual(t, ranks[i-1].Total, ranks[i].Total) - } - }) - } -} - -func TestNodeCacheClient_AddNodeOnlineUser(t *testing.T) { - client := newTestRedisClient(t) - defer cleanupTestData(t, client) - - cache := NewNodeCacheClient(client) - ctx := context.Background() - - tests := []struct { - name string - nodeId int64 - users []NodeOnlineUser - wantErr bool - }{ - { - name: "Add online users normally", - nodeId: 1, - users: []NodeOnlineUser{ - {SID: 1, IP: "192.168.1.1"}, - {SID: 2, IP: "192.168.1.2"}, - }, - wantErr: false, - }, - { - name: "Invalid node ID", - nodeId: 0, - users: []NodeOnlineUser{ - {SID: 1, IP: "192.168.1.1"}, - }, - wantErr: false, - }, - { - name: "Empty user list", - nodeId: 1, - users: []NodeOnlineUser{}, - wantErr: false, - }, - { - name: "Add duplicate user IP", - nodeId: 1, - users: []NodeOnlineUser{ - {SID: 1, IP: "192.168.1.1"}, - {SID: 1, IP: "192.168.1.1"}, - }, - wantErr: false, - }, - { - name: "Multiple IPs for same user", - nodeId: 1, - users: []NodeOnlineUser{ - {SID: 1, IP: "192.168.1.1"}, - {SID: 1, IP: "192.168.1.2"}, - }, - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := cache.AddOnlineUserIP(ctx, tt.users) - if tt.wantErr { - assert.Error(t, err) - return - } - assert.NoError(t, err) - - // Verify data is added correctly - for _, user := range tt.users { - // Get user online IPs - ips, err := cache.GetUserOnlineIp(ctx, user.SID) - assert.NoError(t, err) - assert.Contains(t, ips, user.IP) - - // Verify score is within valid range (current time to 5 minutes later) - score, err := client.ZScore(ctx, fmt.Sprintf(UserOnlineIpCacheKey, user.SID), user.IP).Result() - assert.NoError(t, err) - now := time.Now().Unix() - assert.GreaterOrEqual(t, score, float64(now)) - assert.LessOrEqual(t, score, float64(now+300)) // 5 minutes = 300 seconds - - // Verify key exists - exists, err := client.Exists(ctx, fmt.Sprintf(UserOnlineIpCacheKey, user.SID)).Result() - assert.NoError(t, err) - assert.Equal(t, int64(1), exists) - } - }) - } -} - -func TestNodeCacheClient_GetUserOnlineIp(t *testing.T) { - client := newTestRedisClient(t) - defer cleanupTestData(t, client) - - cache := NewNodeCacheClient(client) - ctx := context.Background() - - // Prepare test data - testData := []struct { - nodeId int64 - users []NodeOnlineUser - }{ - { - nodeId: 1, - users: []NodeOnlineUser{ - {SID: 1, IP: "192.168.1.1"}, - {SID: 1, IP: "192.168.1.2"}, - {SID: 2, IP: "192.168.1.3"}, - }, - }, - } - - // Add test data - for _, data := range testData { - err := cache.AddOnlineUserIP(ctx, data.users) - require.NoError(t, err) - } - - tests := []struct { - name string - uid int64 - wantErr bool - wantIPs []string - }{ - { - name: "Get existing user IPs", - uid: 1, - wantErr: false, - wantIPs: []string{"192.168.1.1", "192.168.1.2"}, - }, - { - name: "Get another user's IPs", - uid: 2, - wantErr: false, - wantIPs: []string{"192.168.1.3"}, - }, - { - name: "Get non-existent user IPs", - uid: 3, - wantErr: false, - wantIPs: []string{}, - }, - { - name: "Invalid user ID", - uid: 0, - wantErr: true, - }, - { - name: "Expired IPs should not be returned", - uid: 1, - wantErr: false, - wantIPs: []string{"192.168.1.1", "192.168.1.2"}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ips, err := cache.GetUserOnlineIp(ctx, tt.uid) - if tt.wantErr { - assert.Error(t, err) - return - } - assert.NoError(t, err) - assert.ElementsMatch(t, tt.wantIPs, ips) - - // Verify all returned IPs are valid - for _, ip := range ips { - score, err := client.ZScore(ctx, fmt.Sprintf(UserOnlineIpCacheKey, tt.uid), ip).Result() - assert.NoError(t, err) - now := time.Now().Unix() - assert.GreaterOrEqual(t, score, float64(now)) - } - }) - } -} - -func TestNodeCacheClient_UpdateNodeOnlineUser(t *testing.T) { - client := newTestRedisClient(t) - defer cleanupTestData(t, client) - - cache := NewNodeCacheClient(client) - ctx := context.Background() - - tests := []struct { - name string - nodeId int64 - users []NodeOnlineUser - wantErr bool - }{ - { - name: "Update online users normally", - nodeId: 1, - users: []NodeOnlineUser{ - {SID: 1, IP: "192.168.1.1"}, - {SID: 2, IP: "192.168.1.2"}, - }, - wantErr: false, - }, - { - name: "Invalid node ID", - nodeId: 0, - users: []NodeOnlineUser{ - {SID: 1, IP: "192.168.1.1"}, - }, - wantErr: true, - }, - { - name: "Empty user list", - nodeId: 1, - users: []NodeOnlineUser{}, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := cache.UpdateNodeOnlineUser(ctx, tt.nodeId, tt.users) - if tt.wantErr { - assert.Error(t, err) - return - } - assert.NoError(t, err) - - // Verify data is updated correctly - data, err := client.Get(ctx, fmt.Sprintf(NodeOnlineUserCacheKey, tt.nodeId)).Result() - assert.NoError(t, err) - - var result map[int64][]string - err = json.Unmarshal([]byte(data), &result) - assert.NoError(t, err) - - // Verify data content - for _, user := range tt.users { - ips, exists := result[user.SID] - assert.True(t, exists) - assert.Contains(t, ips, user.IP) - } - }) - } -} diff --git a/internal/model/cache/types.go b/internal/model/cache/types.go deleted file mode 100644 index 89b6144..0000000 --- a/internal/model/cache/types.go +++ /dev/null @@ -1,34 +0,0 @@ -package cache - -type NodeOnlineUser struct { - SID int64 - IP string -} - -type NodeTodayTrafficRank struct { - ID int64 - Name string - Upload int64 - Download int64 - Total int64 -} - -type UserTodayTrafficRank struct { - SID int64 - Upload int64 - Download int64 - Total int64 -} - -type UserTraffic struct { - UID int64 - Upload int64 - Download int64 -} - -type NodeStatus struct { - Cpu float64 - Mem float64 - Disk float64 - UpdatedAt int64 -} diff --git a/internal/model/client/application.go b/internal/model/client/application.go new file mode 100644 index 0000000..ff121d0 --- /dev/null +++ b/internal/model/client/application.go @@ -0,0 +1,75 @@ +package client + +import ( + "encoding/json" + "time" +) + +type SubscribeApplication struct { + Id int64 `gorm:"primaryKey"` + Name string `gorm:"type:varchar(255);default:'';not null;comment:Application Name"` + Icon string `gorm:"type:MEDIUMTEXT;default:null;comment:Application Icon"` + Description string `gorm:"type:varchar(255);default:null;comment:Application Description"` + Scheme string `gorm:"type:varchar(255);default:'';not null;comment:Scheme"` + UserAgent string `gorm:"type:varchar(255);default:'';not null;comment:User Agent"` + IsDefault bool `gorm:"type:tinyint(1);not null;default:0;comment:Is Default Application"` + SubscribeTemplate string `gorm:"type:MEDIUMTEXT;default:null;comment:Subscribe Template"` + OutputFormat string `gorm:"type:varchar(50);default:'yaml';not null;comment:Output Format"` + DownloadLink string `gorm:"type:text;not null;comment:Download Link"` + CreatedAt time.Time `gorm:"<-:create;comment:Create Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` +} + +func (SubscribeApplication) TableName() string { + return "subscribe_application" +} + +type DownloadLink struct { + IOS string `json:"ios,omitempty"` + Android string `json:"android,omitempty"` + Windows string `json:"windows,omitempty"` + Mac string `json:"mac,omitempty"` + Linux string `json:"linux,omitempty"` + Harmony string `json:"harmony,omitempty"` +} + +// GetDownloadLink returns the download link for the specified platform. +func (d *DownloadLink) GetDownloadLink(platform string) string { + if d == nil { + return "" + } + switch platform { + case "ios": + return d.IOS + case "android": + return d.Android + case "windows": + return d.Windows + case "mac": + return d.Mac + case "linux": + return d.Linux + case "harmony": + return d.Harmony + default: + return "" + } +} + +// Marshal serializes the DownloadLink to JSON format. +func (d *DownloadLink) Marshal() ([]byte, error) { + if d == nil { + var empty DownloadLink + return json.Marshal(empty) + } + return json.Marshal(d) +} + +// Unmarshal parses the JSON-encoded data and stores the result in the DownloadLink. +func (d *DownloadLink) Unmarshal(data []byte) error { + if data == nil || len(data) == 0 { + *d = DownloadLink{} + return nil + } + return json.Unmarshal(data, d) +} diff --git a/internal/model/client/default.go b/internal/model/client/default.go new file mode 100644 index 0000000..c52108f --- /dev/null +++ b/internal/model/client/default.go @@ -0,0 +1,81 @@ +package client + +import ( + "context" + + "gorm.io/gorm" +) + +type ( + Model interface { + subscribeApplicationModel + } + subscribeApplicationModel interface { + Insert(ctx context.Context, data *SubscribeApplication) error + FindOne(ctx context.Context, id int64) (*SubscribeApplication, error) + Update(ctx context.Context, data *SubscribeApplication) error + Delete(ctx context.Context, id int64) error + List(ctx context.Context) ([]*SubscribeApplication, error) + Transaction(ctx context.Context, fn func(db *gorm.DB) error) error + } + DefaultSubscribeApplicationModel struct { + *gorm.DB + } +) + +func NewSubscribeApplicationModel(db *gorm.DB) Model { + return &DefaultSubscribeApplicationModel{ + DB: db, + } +} + +func (m *DefaultSubscribeApplicationModel) Insert(ctx context.Context, data *SubscribeApplication) error { + if err := m.WithContext(ctx).Model(&SubscribeApplication{}).Create(data).Error; err != nil { + return err + } + return nil +} + +func (m *DefaultSubscribeApplicationModel) FindOne(ctx context.Context, id int64) (*SubscribeApplication, error) { + var resp SubscribeApplication + if err := m.WithContext(ctx).Model(&SubscribeApplication{}).Where("id = ?", id).First(&resp).Error; err != nil { + return nil, err + } + return &resp, nil +} + +func (m *DefaultSubscribeApplicationModel) Update(ctx context.Context, data *SubscribeApplication) error { + if _, err := m.FindOne(ctx, data.Id); err != nil { + return err + } + if err := m.WithContext(ctx).Model(&SubscribeApplication{}).Where("`id` = ?", data.Id).Save(data).Error; err != nil { + return err + } + return nil +} + +func (m *DefaultSubscribeApplicationModel) Delete(ctx context.Context, id int64) error { + if err := m.WithContext(ctx).Model(&SubscribeApplication{}).Where("`id` = ?", id).Delete(&SubscribeApplication{}).Error; err != nil { + return err + } + return nil +} + +func (m *DefaultSubscribeApplicationModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error { + tx := m.WithContext(ctx).Begin() + if err := fn(tx); err != nil { + if rbErr := tx.Rollback().Error; rbErr != nil { + return rbErr + } + return err + } + return tx.Commit().Error +} + +func (m *DefaultSubscribeApplicationModel) List(ctx context.Context) ([]*SubscribeApplication, error) { + var resp []*SubscribeApplication + if err := m.WithContext(ctx).Find(&resp).Error; err != nil { + return nil, err + } + return resp, nil +} diff --git a/internal/model/log/default.go b/internal/model/log/default.go index 5b8d731..242891a 100644 --- a/internal/model/log/default.go +++ b/internal/model/log/default.go @@ -6,74 +6,50 @@ import ( "gorm.io/gorm" ) -var _ Model = (*customLogModel)(nil) +var _ Model = (*customSystemLogModel)(nil) type ( Model interface { - messageLogModel + systemLogModel + customSystemLogLogicModel } - messageLogModel interface { - InsertMessageLog(ctx context.Context, data *MessageLog) error - FindOneMessageLog(ctx context.Context, id int64) (*MessageLog, error) - UpdateMessageLog(ctx context.Context, data *MessageLog) error - DeleteMessageLog(ctx context.Context, id int64) error - FindMessageLogList(ctx context.Context, page, size int, filter MessageLogFilterParams) (int64, []*MessageLog, error) + systemLogModel interface { + Insert(ctx context.Context, data *SystemLog) error + FindOne(ctx context.Context, id int64) (*SystemLog, error) + Update(ctx context.Context, data *SystemLog) error + Delete(ctx context.Context, id int64) error } - - customLogModel struct { + customSystemLogModel struct { *defaultLogModel } defaultLogModel struct { - Connection *gorm.DB + *gorm.DB } ) -func newLogModel(db *gorm.DB) *defaultLogModel { +func newSystemLogModel(db *gorm.DB) *defaultLogModel { return &defaultLogModel{ - Connection: db, + DB: db, } } -func (m *defaultLogModel) InsertMessageLog(ctx context.Context, data *MessageLog) error { - return m.Connection.WithContext(ctx).Create(&data).Error +func (m *defaultLogModel) Insert(ctx context.Context, data *SystemLog) error { + return m.WithContext(ctx).Create(data).Error } -func (m *defaultLogModel) FindOneMessageLog(ctx context.Context, id int64) (*MessageLog, error) { - var resp MessageLog - err := m.Connection.WithContext(ctx).Model(&MessageLog{}).Where("`id` = ?", id).First(&resp).Error - return &resp, err +func (m *defaultLogModel) FindOne(ctx context.Context, id int64) (*SystemLog, error) { + var log SystemLog + err := m.WithContext(ctx).Where("id = ?", id).First(&log).Error + if err != nil { + return nil, err + } + return &log, nil } -func (m *defaultLogModel) UpdateMessageLog(ctx context.Context, data *MessageLog) error { - return m.Connection.WithContext(ctx).Model(&MessageLog{}).Where("id = ?", data.Id).Updates(data).Error +func (m *defaultLogModel) Update(ctx context.Context, data *SystemLog) error { + return m.WithContext(ctx).Where("`id` = ?", data.Id).Save(data).Error } -func (m *defaultLogModel) DeleteMessageLog(ctx context.Context, id int64) error { - return m.Connection.WithContext(ctx).Model(&MessageLog{}).Where("id = ?", id).Delete(&MessageLog{}).Error -} - -func (m *defaultLogModel) FindMessageLogList(ctx context.Context, page, size int, filter MessageLogFilterParams) (int64, []*MessageLog, error) { - var list []*MessageLog - var total int64 - conn := m.Connection.WithContext(ctx).Model(&MessageLog{}) - if filter.Type != "" { - conn = conn.Where("`type` = ?", filter.Type) - } - if filter.Platform != "" { - conn = conn.Where("`platform` = ?", filter.Platform) - } - if filter.To != "" { - conn = conn.Where("`to` LIKE ?", "%"+filter.To+"%") - } - if filter.Subject != "" { - conn = conn.Where("`subject` LIKE ?", "%"+filter.Subject+"%") - } - if filter.Content != "" { - conn = conn.Where("`content` = ?", "%"+filter.Content+"%") - } - if filter.Status > 0 { - conn = conn.Where("`status` = ?", filter.Status) - } - err := conn.Count(&total).Offset((page - 1) * size).Limit(size).Find(&list).Error - return total, list, err +func (m *defaultLogModel) Delete(ctx context.Context, id int64) error { + return m.WithContext(ctx).Where("`id` = ?", id).Delete(&SystemLog{}).Error } diff --git a/internal/model/log/log.go b/internal/model/log/log.go index af66746..1afd13d 100644 --- a/internal/model/log/log.go +++ b/internal/model/log/log.go @@ -1,45 +1,425 @@ package log -import "time" - -type MessageType int - -const ( - Email MessageType = iota + 1 - Mobile +import ( + "encoding/json" + "time" ) -func (t MessageType) String() string { - switch t { - case Email: - return "email" - case Mobile: - return "mobile" - } - return "unknown" +type Type uint8 + +/* + +Log Types: + 1X Message Logs + 2X Subscription Logs + 3X User Logs + 4X Traffic Ranking Logs +*/ + +const ( + TypeEmailMessage Type = 10 // Message log + TypeMobileMessage Type = 11 // Mobile message log + TypeSubscribe Type = 20 // Subscription log + TypeSubscribeTraffic Type = 21 // Subscription traffic log + TypeServerTraffic Type = 22 // Server traffic log + TypeResetSubscribe Type = 23 // Reset subscription log + TypeLogin Type = 30 // Login log + TypeRegister Type = 31 // Registration log + TypeBalance Type = 32 // Balance log + TypeCommission Type = 33 // Commission log + TypeGift Type = 34 // Gift log + TypeUserTrafficRank Type = 40 // Top 10 User traffic rank log + TypeServerTrafficRank Type = 41 // Top 10 Server traffic rank log + TypeTrafficStat Type = 42 // Daily traffic statistics log +) +const ( + ResetSubscribeTypeAuto uint16 = 231 // Auto reset + ResetSubscribeTypeAdvance uint16 = 232 // Advance reset + ResetSubscribeTypePaid uint16 = 233 // Paid reset + ResetSubscribeTypeQuota uint16 = 234 // Quota reset + BalanceTypeRecharge uint16 = 321 // Recharge + BalanceTypeWithdraw uint16 = 322 // Withdraw + BalanceTypePayment uint16 = 323 // Payment + BalanceTypeRefund uint16 = 324 // Refund + BalanceTypeAdjust uint16 = 326 // Admin Adjust + BalanceTypeReward uint16 = 325 // Reward + CommissionTypePurchase uint16 = 331 // Purchase + CommissionTypeRenewal uint16 = 332 // Renewal + CommissionTypeRefund uint16 = 333 // Refund + CommissionTypeWithdraw uint16 = 334 // withdraw + CommissionTypeAdjust uint16 = 335 // Admin Adjust + CommissionTypeConvertBalance uint16 = 336 // Convert to Balance + GiftTypeIncrease uint16 = 341 // Increase + GiftTypeReduce uint16 = 342 // Reduce +) + +// Uint8 converts Type to uint8. +func (t Type) Uint8() uint8 { + return uint8(t) } -type MessageLog struct { - Id int64 `gorm:"primaryKey"` - Type string `gorm:"type:varchar(50);not null;default:'email';comment:Message Type"` - Platform string `gorm:"type:varchar(50);not null;default:'smtp';comment:Platform"` - To string `gorm:"type:text;not null;comment:To"` - Subject string `gorm:"type:varchar(255);not null;default:'';comment:Subject"` - Content string `gorm:"type:text;comment:Content"` - Status int `gorm:"type:tinyint(1);not null;default:0;comment:Status"` +// SystemLog represents a log entry in the system. +type SystemLog struct { + Id int64 `gorm:"primaryKey;AUTO_INCREMENT"` + Type uint8 `gorm:"index:idx_type;type:tinyint(1);not null;default:0;comment:Log Type: 1: Email Message 2: Mobile Message 3: Subscribe 4: Subscribe Traffic 5: Server Traffic 6: Login 7: Register 8: Balance 9: Commission 10: Reset Subscribe 11: Gift"` + Date string `gorm:"type:varchar(20);default:null;comment:Log Date"` + ObjectID int64 `gorm:"index:idx_object_id;type:bigint(20);not null;default:0;comment:Object ID"` + Content string `gorm:"type:text;not null;comment:Log Content"` CreatedAt time.Time `gorm:"<-:create;comment:Create Time"` - UpdatedAt time.Time `gorm:"comment:Update Time"` } -func (m *MessageLog) TableName() string { - return "message_log" +// TableName returns the name of the table for SystemLogs. +func (SystemLog) TableName() string { + return "system_logs" } -type MessageLogFilterParams struct { - Type string - Platform string - To string - Subject string - Content string - Status int +// Message represents a message log entry. +type Message struct { + To string `json:"to"` + Subject string `json:"subject,omitempty"` + Content map[string]interface{} `json:"content"` + Platform string `json:"platform"` + Template string `json:"template"` + Status uint8 `json:"status"` // 1: Sent, 2: Failed +} + +// Marshal implements the json.Marshaler interface for Message. +func (m *Message) Marshal() ([]byte, error) { + type Alias Message + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(m), + }) +} + +// Unmarshal implements the json.Unmarshaler interface for Message. +func (m *Message) Unmarshal(data []byte) error { + type Alias Message + aux := (*Alias)(m) + return json.Unmarshal(data, aux) +} + +// Traffic represents a subscription traffic log entry. +type Traffic struct { + Download int64 `json:"download"` + Upload int64 `json:"upload"` +} + +// Marshal implements the json.Marshaler interface for SubscribeTraffic. +func (s *Traffic) Marshal() ([]byte, error) { + type Alias Traffic + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(s), + }) +} + +// Unmarshal implements the json.Unmarshaler interface for SubscribeTraffic. +func (s *Traffic) Unmarshal(data []byte) error { + type Alias Traffic + aux := (*Alias)(s) + return json.Unmarshal(data, aux) +} + +// Login represents a login log entry. +type Login struct { + Method string `json:"method"` + LoginIP string `json:"login_ip"` + UserAgent string `json:"user_agent"` + Success bool `json:"success"` + Timestamp int64 `json:"timestamp"` +} + +// Marshal implements the json.Marshaler interface for Login. +func (l *Login) Marshal() ([]byte, error) { + type Alias Login + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(l), + }) +} + +// Unmarshal implements the json.Unmarshaler interface for Login. +func (l *Login) Unmarshal(data []byte) error { + type Alias Login + aux := (*Alias)(l) + return json.Unmarshal(data, aux) +} + +// Register represents a registration log entry. +type Register struct { + AuthMethod string `json:"auth_method"` + Identifier string `json:"identifier"` + RegisterIP string `json:"register_ip"` + UserAgent string `json:"user_agent"` + Timestamp int64 `json:"timestamp"` +} + +// Marshal implements the json.Marshaler interface for Register. +func (r *Register) Marshal() ([]byte, error) { + type Alias Register + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(r), + }) +} + +// Unmarshal implements the json.Unmarshaler interface for Register. + +func (r *Register) Unmarshal(data []byte) error { + type Alias Register + aux := (*Alias)(r) + return json.Unmarshal(data, aux) +} + +// Subscribe represents a subscription log entry. +type Subscribe struct { + Token string `json:"token"` + UserAgent string `json:"user_agent"` + ClientIP string `json:"client_ip"` + UserSubscribeId int64 `json:"user_subscribe_id"` +} + +// Marshal implements the json.Marshaler interface for Subscribe. +func (s *Subscribe) Marshal() ([]byte, error) { + type Alias Subscribe + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(s), + }) +} + +// Unmarshal implements the json.Unmarshaler interface for Subscribe. +func (s *Subscribe) Unmarshal(data []byte) error { + type Alias Subscribe + aux := (*Alias)(s) + return json.Unmarshal(data, aux) +} + +// ResetSubscribe represents a reset subscription log entry. +type ResetSubscribe struct { + Type uint16 `json:"type"` + UserId int64 `json:"user_id"` + OrderNo string `json:"order_no,omitempty"` + Timestamp int64 `json:"timestamp"` +} + +// Marshal implements the json.Marshaler interface for ResetSubscribe. +func (r *ResetSubscribe) Marshal() ([]byte, error) { + type Alias ResetSubscribe + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(r), + }) +} + +// Unmarshal implements the json.Unmarshaler interface for ResetSubscribe. +func (r *ResetSubscribe) Unmarshal(data []byte) error { + type Alias ResetSubscribe + aux := (*Alias)(r) + return json.Unmarshal(data, aux) +} + +// Balance represents a balance log entry. +type Balance struct { + Type uint16 `json:"type"` + Amount int64 `json:"amount"` + OrderNo string `json:"order_no,omitempty"` + Balance int64 `json:"balance"` + Timestamp int64 `json:"timestamp"` +} + +// Marshal implements the json.Marshaler interface for Balance. +func (b *Balance) Marshal() ([]byte, error) { + type Alias Balance + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(b), + }) +} + +// Unmarshal implements the json.Unmarshaler interface for Balance. +func (b *Balance) Unmarshal(data []byte) error { + type Alias Balance + aux := (*Alias)(b) + return json.Unmarshal(data, aux) +} + +// Commission represents a commission log entry. +type Commission struct { + Type uint16 `json:"type"` + Amount int64 `json:"amount"` + OrderNo string `json:"order_no"` + Timestamp int64 `json:"timestamp"` +} + +// Marshal implements the json.Marshaler interface for Commission. +func (c *Commission) Marshal() ([]byte, error) { + type Alias Commission + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(c), + }) +} + +// Unmarshal implements the json.Unmarshaler interface for Commission. +func (c *Commission) Unmarshal(data []byte) error { + type Alias Commission + aux := (*Alias)(c) + return json.Unmarshal(data, aux) +} + +// Gift represents a gift log entry. +type Gift struct { + Type uint16 `json:"type"` + OrderNo string `json:"order_no"` + SubscribeId int64 `json:"subscribe_id"` + Amount int64 `json:"amount"` + Balance int64 `json:"balance"` + Remark string `json:"remark,omitempty"` + Timestamp int64 `json:"timestamp"` +} + +// Marshal implements the json.Marshaler interface for Gift. +func (g *Gift) Marshal() ([]byte, error) { + type Alias Gift + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(g), + }) +} + +// Unmarshal implements the json.Unmarshaler interface for Gift. +func (g *Gift) Unmarshal(data []byte) error { + type Alias Gift + aux := (*Alias)(g) + return json.Unmarshal(data, aux) +} + +// UserTraffic represents a user traffic log entry. +type UserTraffic struct { + SubscribeId int64 `json:"subscribe_id"` // Subscribe ID + UserId int64 `json:"user_id"` // User ID + Upload int64 `json:"upload"` // Upload traffic in bytes + Download int64 `json:"download"` // Download traffic in bytes + Total int64 `json:"total"` // Total traffic in bytes (Upload + Download) +} + +// Marshal implements the json.Marshaler interface for UserTraffic. +func (u *UserTraffic) Marshal() ([]byte, error) { + type Alias UserTraffic + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(u), + }) +} + +// Unmarshal implements the json.Unmarshaler interface for UserTraffic. +func (u *UserTraffic) Unmarshal(data []byte) error { + type Alias UserTraffic + aux := (*Alias)(u) + return json.Unmarshal(data, aux) +} + +// UserTrafficRank represents a user traffic rank entry. +type UserTrafficRank struct { + Rank map[uint8]UserTraffic `json:"rank"` // Key is rank ,type is UserTraffic +} + +// Marshal implements the json.Marshaler interface for UserTrafficRank. +func (u *UserTrafficRank) Marshal() ([]byte, error) { + type Alias UserTrafficRank + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(u), + }) +} + +// Unmarshal implements the json.Unmarshaler interface for UserTrafficRank. +func (u *UserTrafficRank) Unmarshal(data []byte) error { + type Alias UserTrafficRank + aux := (*Alias)(u) + return json.Unmarshal(data, aux) +} + +// ServerTraffic represents a server traffic log entry. +type ServerTraffic struct { + ServerId int64 `json:"server_id"` // Server ID + Upload int64 `json:"upload"` // Upload traffic in bytes + Download int64 `json:"download"` // Download traffic in bytes + Total int64 `json:"total"` // Total traffic in bytes (Upload + Download) +} + +// Marshal implements the json.Marshaler interface for ServerTraffic. +func (s *ServerTraffic) Marshal() ([]byte, error) { + type Alias ServerTraffic + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(s), + }) +} + +// Unmarshal implements the json.Unmarshaler interface for ServerTraffic. +func (s *ServerTraffic) Unmarshal(data []byte) error { + type Alias ServerTraffic + aux := (*Alias)(s) + return json.Unmarshal(data, aux) +} + +// ServerTrafficRank represents a server traffic rank entry. +type ServerTrafficRank struct { + Rank map[uint8]ServerTraffic `json:"rank"` // Key is rank ,type is ServerTraffic +} + +// Marshal implements the json.Marshaler interface for ServerTrafficRank. +func (s *ServerTrafficRank) Marshal() ([]byte, error) { + type Alias ServerTrafficRank + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(s), + }) +} + +// Unmarshal implements the json.Unmarshaler interface for ServerTrafficRank. +func (s *ServerTrafficRank) Unmarshal(data []byte) error { + type Alias ServerTrafficRank + aux := (*Alias)(s) + return json.Unmarshal(data, aux) +} + +// TrafficStat represents a daily traffic statistics log entry. +type TrafficStat struct { + Upload int64 `json:"upload"` + Download int64 `json:"download"` + Total int64 `json:"total"` +} + +// Marshal implements the json.Marshaler interface for TrafficStat. +func (t *TrafficStat) Marshal() ([]byte, error) { + type Alias TrafficStat + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(t), + }) +} + +// Unmarshal implements the json.Unmarshaler interface for TrafficStat. +func (t *TrafficStat) Unmarshal(data []byte) error { + type Alias TrafficStat + aux := (*Alias)(t) + return json.Unmarshal(data, aux) } diff --git a/internal/model/log/model.go b/internal/model/log/model.go index 8bacf15..783b6b8 100644 --- a/internal/model/log/model.go +++ b/internal/model/log/model.go @@ -1,9 +1,63 @@ package log import ( + "context" + "gorm.io/gorm" ) -func NewModel(conn *gorm.DB) Model { - return newLogModel(conn) +func NewModel(db *gorm.DB) Model { + return &customSystemLogModel{ + defaultLogModel: newSystemLogModel(db), + } +} + +type FilterParams struct { + Page int + Size int + Type uint8 + Data string + Search string + ObjectID int64 +} + +type customSystemLogLogicModel interface { + FilterSystemLog(ctx context.Context, filter *FilterParams) ([]*SystemLog, int64, error) +} + +func (m *customSystemLogModel) FilterSystemLog(ctx context.Context, filter *FilterParams) ([]*SystemLog, int64, error) { + tx := m.WithContext(ctx).Model(&SystemLog{}).Order("id DESC") + if filter == nil { + filter = &FilterParams{ + Page: 1, + Size: 10, + } + } + + if filter.Page < 1 { + filter.Page = 1 + } + if filter.Size < 1 { + filter.Size = 10 + } + + if filter.Type != 0 { + tx = tx.Where("`type` = ?", filter.Type) + } + + if filter.Data != "" { + tx = tx.Where("`date` = ?", filter.Data) + } + + if filter.ObjectID != 0 { + tx = tx.Where("`object_id` = ?", filter.ObjectID) + } + if filter.Search != "" { + tx = tx.Where("`content` LIKE ?", "%"+filter.Search+"%") + } + + var total int64 + var logs []*SystemLog + err := tx.Count(&total).Limit(filter.Size).Offset((filter.Page - 1) * filter.Size).Find(&logs).Error + return logs, total, err } diff --git a/internal/model/node/cache.go b/internal/model/node/cache.go new file mode 100644 index 0000000..3d16c78 --- /dev/null +++ b/internal/model/node/cache.go @@ -0,0 +1,163 @@ +package node + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/pkg/errors" + "github.com/redis/go-redis/v9" +) + +type ( + customCacheLogicModel interface { + StatusCache(ctx context.Context, serverId int64) (Status, error) + UpdateStatusCache(ctx context.Context, serverId int64, status *Status) error + OnlineUserSubscribe(ctx context.Context, serverId int64, protocol string) (OnlineUserSubscribe, error) + UpdateOnlineUserSubscribe(ctx context.Context, serverId int64, protocol string, subscribe OnlineUserSubscribe) error + OnlineUserSubscribeGlobal(ctx context.Context) (int64, error) + UpdateOnlineUserSubscribeGlobal(ctx context.Context, subscribe OnlineUserSubscribe) error + } + + Status struct { + Cpu float64 `json:"cpu"` + Mem float64 `json:"mem"` + Disk float64 `json:"disk"` + UpdatedAt int64 `json:"updated_at"` + } + + OnlineUserSubscribe map[int64][]string +) + +// Marshal to json string +func (s *Status) Marshal() string { + type Alias Status + data, _ := json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(s), + }) + return string(data) +} + +// Unmarshal from json string +func (s *Status) Unmarshal(data string) error { + type Alias Status + aux := &struct { + *Alias + }{ + Alias: (*Alias)(s), + } + return json.Unmarshal([]byte(data), &aux) +} + +const ( + Expiry = 300 * time.Second // Cache expiry time in seconds + StatusCacheKey = "node:status:%d" // Node status cache key format (Server ID and protocol) Example: node:status:1:shadowsocks + OnlineUserCacheKeyWithSubscribe = "node:online:subscribe:%d:%s" // Online user subscribe cache key format (Server ID and protocol) Example: node:online:subscribe:1:shadowsocks + OnlineUserSubscribeCacheKeyWithGlobal = "node:online:subscribe:global" // Online user global subscribe cache key +) + +// UpdateStatusCache Update server status to cache +func (m *customServerModel) UpdateStatusCache(ctx context.Context, serverId int64, status *Status) error { + key := fmt.Sprintf(StatusCacheKey, serverId) + return m.Cache.Set(ctx, key, status.Marshal(), Expiry).Err() + +} + +// DeleteStatusCache Delete server status from cache +func (m *customServerModel) DeleteStatusCache(ctx context.Context, serverId int64) error { + key := fmt.Sprintf(StatusCacheKey, serverId) + return m.Cache.Del(ctx, key).Err() +} + +// StatusCache Get server status from cache +func (m *customServerModel) StatusCache(ctx context.Context, serverId int64) (Status, error) { + var status Status + key := fmt.Sprintf(StatusCacheKey, serverId) + + result, err := m.Cache.Get(ctx, key).Result() + if err != nil { + if errors.Is(err, redis.Nil) { + return status, nil + } + return status, err + } + if result == "" { + return status, nil + } + err = status.Unmarshal(result) + return status, err +} + +// OnlineUserSubscribe Get online user subscribe +func (m *customServerModel) OnlineUserSubscribe(ctx context.Context, serverId int64, protocol string) (OnlineUserSubscribe, error) { + key := fmt.Sprintf(OnlineUserCacheKeyWithSubscribe, serverId, protocol) + result, err := m.Cache.Get(ctx, key).Result() + if err != nil { + if errors.Is(err, redis.Nil) { + return OnlineUserSubscribe{}, nil + } + return nil, err + } + if result == "" { + return OnlineUserSubscribe{}, nil + } + var subscribe OnlineUserSubscribe + err = json.Unmarshal([]byte(result), &subscribe) + return subscribe, err +} + +// UpdateOnlineUserSubscribe Update online user subscribe +func (m *customServerModel) UpdateOnlineUserSubscribe(ctx context.Context, serverId int64, protocol string, subscribe OnlineUserSubscribe) error { + key := fmt.Sprintf(OnlineUserCacheKeyWithSubscribe, serverId, protocol) + data, err := json.Marshal(subscribe) + if err != nil { + return err + } + return m.Cache.Set(ctx, key, data, Expiry).Err() +} + +// DeleteOnlineUserSubscribe Delete online user subscribe +func (m *customServerModel) DeleteOnlineUserSubscribe(ctx context.Context, serverId int64, protocol string) error { + key := fmt.Sprintf(OnlineUserCacheKeyWithSubscribe, serverId, protocol) + return m.Cache.Del(ctx, key).Err() +} + +// OnlineUserSubscribeGlobal Get global online user subscribe count +func (m *customServerModel) OnlineUserSubscribeGlobal(ctx context.Context) (int64, error) { + now := time.Now().Unix() + // Clear expired data + if err := m.Cache.ZRemRangeByScore(ctx, OnlineUserSubscribeCacheKeyWithGlobal, "-inf", fmt.Sprintf("%d", now)).Err(); err != nil { + return 0, err + } + return m.Cache.ZCard(ctx, OnlineUserSubscribeCacheKeyWithGlobal).Result() +} + +// UpdateOnlineUserSubscribeGlobal Update global online user subscribe count +func (m *customServerModel) UpdateOnlineUserSubscribeGlobal(ctx context.Context, subscribe OnlineUserSubscribe) error { + now := time.Now() + expireTime := now.Add(5 * time.Minute).Unix() // set expire time 5 minutes later + + pipe := m.Cache.Pipeline() + + // Clear expired data + pipe.ZRemRangeByScore(ctx, OnlineUserSubscribeCacheKeyWithGlobal, "-inf", fmt.Sprintf("%d", now.Unix())) + // Add or update each subscribe with new expire time + for sub := range subscribe { + // Use ZAdd to add or update the member with new score (expire time) + pipe.ZAdd(ctx, OnlineUserSubscribeCacheKeyWithGlobal, redis.Z{ + Score: float64(expireTime), + Member: sub, + }) + } + + _, err := pipe.Exec(ctx) + return err +} + +// DeleteOnlineUserSubscribeGlobal Delete global online user subscribe count +func (m *customServerModel) DeleteOnlineUserSubscribeGlobal(ctx context.Context) error { + return m.Cache.Del(ctx, OnlineUserSubscribeCacheKeyWithGlobal).Err() +} diff --git a/internal/model/node/default.go b/internal/model/node/default.go new file mode 100644 index 0000000..ebdf331 --- /dev/null +++ b/internal/model/node/default.go @@ -0,0 +1,132 @@ +package node + +import ( + "context" + + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +var _ Model = (*customServerModel)(nil) + +//goland:noinspection GoNameStartsWithPackageName +type ( + Model interface { + serverModel + NodeModel + customCacheLogicModel + customServerLogicModel + } + serverModel interface { + InsertServer(ctx context.Context, data *Server, tx ...*gorm.DB) error + FindOneServer(ctx context.Context, id int64) (*Server, error) + UpdateServer(ctx context.Context, data *Server, tx ...*gorm.DB) error + DeleteServer(ctx context.Context, id int64, tx ...*gorm.DB) error + Transaction(ctx context.Context, fn func(db *gorm.DB) error) error + QueryServerList(ctx context.Context, ids []int64) (servers []*Server, err error) + } + + NodeModel interface { + InsertNode(ctx context.Context, data *Node, tx ...*gorm.DB) error + FindOneNode(ctx context.Context, id int64) (*Node, error) + UpdateNode(ctx context.Context, data *Node, tx ...*gorm.DB) error + DeleteNode(ctx context.Context, id int64, tx ...*gorm.DB) error + } + + customServerModel struct { + *defaultServerModel + } + defaultServerModel struct { + *gorm.DB + Cache *redis.Client + } +) + +func newServerModel(db *gorm.DB, cache *redis.Client) *defaultServerModel { + return &defaultServerModel{ + DB: db, + Cache: cache, + } +} + +// NewModel returns a model for the database table. +func NewModel(conn *gorm.DB, cache *redis.Client) Model { + return &customServerModel{ + defaultServerModel: newServerModel(conn, cache), + } +} + +func (m *defaultServerModel) InsertServer(ctx context.Context, data *Server, tx ...*gorm.DB) error { + db := m.DB + if len(tx) > 0 { + db = tx[0] + } + return db.WithContext(ctx).Create(data).Error +} + +func (m *defaultServerModel) FindOneServer(ctx context.Context, id int64) (*Server, error) { + var server Server + err := m.WithContext(ctx).Model(&Server{}).Where("id = ?", id).First(&server).Error + return &server, err +} + +func (m *defaultServerModel) UpdateServer(ctx context.Context, data *Server, tx ...*gorm.DB) error { + _, err := m.FindOneServer(ctx, data.Id) + if err != nil { + return err + } + + db := m.DB + if len(tx) > 0 { + db = tx[0] + } + return db.WithContext(ctx).Where("`id` = ?", data.Id).Save(data).Error + +} + +func (m *defaultServerModel) DeleteServer(ctx context.Context, id int64, tx ...*gorm.DB) error { + db := m.DB + if len(tx) > 0 { + db = tx[0] + } + return db.WithContext(ctx).Where("`id` = ?", id).Delete(&Server{}).Error +} + +func (m *defaultServerModel) InsertNode(ctx context.Context, data *Node, tx ...*gorm.DB) error { + db := m.DB + if len(tx) > 0 { + db = tx[0] + } + return db.WithContext(ctx).Create(data).Error +} + +func (m *defaultServerModel) FindOneNode(ctx context.Context, id int64) (*Node, error) { + var node Node + err := m.WithContext(ctx).Model(&Node{}).Where("id = ?", id).First(&node).Error + return &node, err +} + +func (m *defaultServerModel) UpdateNode(ctx context.Context, data *Node, tx ...*gorm.DB) error { + _, err := m.FindOneNode(ctx, data.Id) + if err != nil { + return err + } + + db := m.DB + if len(tx) > 0 { + db = tx[0] + } + return db.WithContext(ctx).Where("`id` = ?", data.Id).Save(data).Error +} + +func (m *defaultServerModel) DeleteNode(ctx context.Context, id int64, tx ...*gorm.DB) error { + db := m.DB + if len(tx) > 0 { + db = tx[0] + } + return db.WithContext(ctx).Where("`id` = ?", id).Delete(&Node{}).Error +} + +func (m *defaultServerModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error { + return m.WithContext(ctx).Transaction(fn) +} diff --git a/internal/model/node/model.go b/internal/model/node/model.go new file mode 100644 index 0000000..ddfa736 --- /dev/null +++ b/internal/model/node/model.go @@ -0,0 +1,191 @@ +package node + +import ( + "context" + "fmt" + "strings" + + "github.com/perfect-panel/server/pkg/tool" + "gorm.io/gorm" +) + +type customServerLogicModel interface { + FilterServerList(ctx context.Context, params *FilterParams) (int64, []*Server, error) + FilterNodeList(ctx context.Context, params *FilterNodeParams) (int64, []*Node, error) + ClearNodeCache(ctx context.Context, params *FilterNodeParams) error +} + +const ( + // ServerUserListCacheKey Server User List Cache Key + ServerUserListCacheKey = "server:user:" + + // ServerConfigCacheKey Server Config Cache Key + ServerConfigCacheKey = "server:config:" +) + +// FilterParams Filter Server Params +type FilterParams struct { + Page int + Size int + Ids []int64 // Server IDs + Search string +} + +type FilterNodeParams struct { + Page int // Page Number + Size int // Page Size + NodeId []int64 // Node IDs + ServerId []int64 // Server IDs + Tag []string // Tags + Search string // Search Address or Name + Protocol string // Protocol + Preload bool // Preload Server + Enabled *bool // Enabled +} + +// FilterServerList Filter Server List +func (m *customServerModel) FilterServerList(ctx context.Context, params *FilterParams) (int64, []*Server, error) { + var servers []*Server + var total int64 + query := m.WithContext(ctx).Model(&Server{}) + if params == nil { + params = &FilterParams{ + Page: 1, + Size: 10, + } + } + if params.Search != "" { + s := "%" + params.Search + "%" + query = query.Where("`name` LIKE ? OR `address` LIKE ?", s, s) + } + if len(params.Ids) > 0 { + query = query.Where("id IN ?", params.Ids) + } + err := query.Count(&total).Order("sort ASC").Limit(params.Size).Offset((params.Page - 1) * params.Size).Find(&servers).Error + return total, servers, err +} + +func (m *customServerModel) QueryServerList(ctx context.Context, ids []int64) (servers []*Server, err error) { + query := m.WithContext(ctx).Model(&Server{}) + err = query.Where("id IN (?)", ids).Find(&servers).Error + return +} + +// FilterNodeList Filter Node List +func (m *customServerModel) FilterNodeList(ctx context.Context, params *FilterNodeParams) (int64, []*Node, error) { + var nodes []*Node + var total int64 + query := m.WithContext(ctx).Model(&Node{}) + if params == nil { + params = &FilterNodeParams{ + Page: 1, + Size: 10, + } + } + if params.Search != "" { + s := "%" + params.Search + "%" + query = query.Where("`name` LIKE ? OR `address` LIKE ? OR `tags` LIKE ? OR `port` LIKE ? ", s, s, s, s) + } + if len(params.NodeId) > 0 { + query = query.Where("id IN ?", params.NodeId) + } + if len(params.ServerId) > 0 { + query = query.Where("server_id IN ?", params.ServerId) + } + if len(params.Tag) > 0 { + query = query.Scopes(InSet("tags", params.Tag)) + } + if params.Protocol != "" { + query = query.Where("protocol = ?", params.Protocol) + } + + if params.Enabled != nil { + query = query.Where("enabled = ?", *params.Enabled) + } + + if params.Preload { + query = query.Preload("Server") + } + + err := query.Count(&total).Order("sort ASC").Limit(params.Size).Offset((params.Page - 1) * params.Size).Find(&nodes).Error + return total, nodes, err +} + +// ClearNodeCache Clear Node Cache +func (m *customServerModel) ClearNodeCache(ctx context.Context, params *FilterNodeParams) error { + _, nodes, err := m.FilterNodeList(ctx, params) + if err != nil { + return err + } + var cacheKeys []string + for _, node := range nodes { + cacheKeys = append(cacheKeys, fmt.Sprintf("%s%d", ServerUserListCacheKey, node.ServerId)) + if node.Protocol != "" { + var cursor uint64 + for { + keys, newCursor, err := m.Cache.Scan(ctx, cursor, fmt.Sprintf("%s%d*", ServerConfigCacheKey, node.ServerId), 100).Result() + if err != nil { + return err + } + if len(keys) > 0 { + cacheKeys = append(keys, keys...) + } + cursor = newCursor + if cursor == 0 { + break + } + } + } + } + + if len(cacheKeys) > 0 { + cacheKeys = tool.RemoveDuplicateElements(cacheKeys...) + return m.Cache.Del(ctx, cacheKeys...).Err() + } + return nil +} + +// ClearServerCache Clear Server Cache +func (m *customServerModel) ClearServerCache(ctx context.Context, serverId int64) error { + var cacheKeys []string + cacheKeys = append(cacheKeys, fmt.Sprintf("%s%d", ServerUserListCacheKey, serverId)) + var cursor uint64 + for { + keys, newCursor, err := m.Cache.Scan(ctx, 0, fmt.Sprintf("%s%d*", ServerConfigCacheKey, serverId), 100).Result() + if err != nil { + return err + } + if len(keys) > 0 { + cacheKeys = append(cacheKeys, keys...) + } + cursor = newCursor + if cursor == 0 { + break + } + } + + if len(cacheKeys) > 0 { + cacheKeys = tool.RemoveDuplicateElements(cacheKeys...) + return m.Cache.Del(ctx, cacheKeys...).Err() + } + return nil +} + +// InSet 支持多值 OR 查询 +func InSet(field string, values []string) func(db *gorm.DB) *gorm.DB { + return func(db *gorm.DB) *gorm.DB { + if len(values) == 0 { + return db + } + + conds := make([]string, len(values)) + args := make([]interface{}, len(values)) + for i, v := range values { + conds[i] = "FIND_IN_SET(?, " + field + ")" + args[i] = v + } + + // 用括号包裹 OR 条件,保证外层 AND 不受影响 + return db.Where("("+strings.Join(conds, " OR ")+")", args...) + } +} diff --git a/internal/model/node/node.go b/internal/model/node/node.go new file mode 100644 index 0000000..89d665d --- /dev/null +++ b/internal/model/node/node.go @@ -0,0 +1,82 @@ +package node + +import ( + "time" + + "github.com/perfect-panel/server/pkg/logger" + "gorm.io/gorm" +) + +type Node struct { + Id int64 `gorm:"primary_key"` + Name string `gorm:"type:varchar(100);not null;default:'';comment:Node Name"` + Tags string `gorm:"type:varchar(255);not null;default:'';comment:Tags"` + Port uint16 `gorm:"not null;default:0;comment:Connect Port"` + Address string `gorm:"type:varchar(255);not null;default:'';comment:Connect Address"` + ServerId int64 `gorm:"not null;default:0;comment:Server ID"` + Server *Server `gorm:"foreignKey:ServerId;references:Id"` + Protocol string `gorm:"type:varchar(100);not null;default:'';comment:Protocol"` + Enabled *bool `gorm:"type:boolean;not null;default:true;comment:Enabled"` + Sort int `gorm:"uniqueIndex;not null;default:0;comment:Sort"` + CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` +} + +func (n *Node) TableName() string { + return "nodes" +} + +func (n *Node) BeforeCreate(tx *gorm.DB) error { + if n.Sort == 0 { + var maxSort int + if err := tx.Model(&Node{}).Select("COALESCE(MAX(sort), 0)").Scan(&maxSort).Error; err != nil { + return err + } + n.Sort = maxSort + 1 + } + return nil +} + +func (n *Node) BeforeDelete(tx *gorm.DB) error { + if err := tx.Exec("UPDATE `nodes` SET sort = sort - 1 WHERE sort > ?", n.Sort).Error; err != nil { + return err + } + return nil +} + +func (n *Node) BeforeUpdate(tx *gorm.DB) error { + var count int64 + if err := tx.Set("gorm:query_option", "FOR UPDATE").Model(&Server{}). + Where("sort = ? AND id != ?", n.Sort, n.Id).Count(&count).Error; err != nil { + return err + } + if count > 1 { + // reorder sort + if err := reorderSortWithNode(tx); err != nil { + logger.Errorf("[Server] BeforeUpdate reorderSort error: %v", err.Error()) + return err + } + // get max sort + var maxSort int + if err := tx.Model(&Server{}).Select("MAX(sort)").Scan(&maxSort).Error; err != nil { + return err + } + n.Sort = maxSort + 1 + } + return nil +} + +func reorderSortWithNode(tx *gorm.DB) error { + var nodes []Node + if err := tx.Order("sort, id").Find(&nodes).Error; err != nil { + return err + } + for i, node := range nodes { + if node.Sort != i+1 { + if err := tx.Exec("UPDATE `nodes` SET sort = ? WHERE id = ?", i+1, node.Id).Error; err != nil { + return err + } + } + } + return nil +} diff --git a/internal/model/node/server.go b/internal/model/node/server.go new file mode 100644 index 0000000..00e433e --- /dev/null +++ b/internal/model/node/server.go @@ -0,0 +1,188 @@ +package node + +import ( + "encoding/json" + "time" + + "github.com/perfect-panel/server/pkg/logger" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type Server struct { + Id int64 `gorm:"primary_key"` + Name string `gorm:"type:varchar(100);not null;default:'';comment:Server Name"` + Country string `gorm:"type:varchar(128);not null;default:'';comment:Country"` + City string `gorm:"type:varchar(128);not null;default:'';comment:City"` + //Ratio float32 `gorm:"type:DECIMAL(4,2);not null;default:0;comment:Traffic Ratio"` + Address string `gorm:"type:varchar(100);not null;default:'';comment:Server Address"` + Sort int `gorm:"type:int;not null;default:0;comment:Sort"` + Protocols string `gorm:"type:text;default:null;comment:Protocol"` + LastReportedAt *time.Time `gorm:"comment:Last Reported Time"` + CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` +} + +func (*Server) TableName() string { + return "servers" +} + +func (m *Server) BeforeCreate(tx *gorm.DB) error { + if m.Sort == 0 { + var maxSort int + if err := tx.Model(&Server{}).Select("COALESCE(MAX(sort), 0)").Scan(&maxSort).Error; err != nil { + return err + } + m.Sort = maxSort + 1 + } + return nil +} + +func (m *Server) BeforeDelete(tx *gorm.DB) error { + if err := tx.Exec("UPDATE `servers` SET sort = sort - 1 WHERE sort > ?", m.Sort).Error; err != nil { + return err + } + return nil +} + +func (m *Server) BeforeUpdate(tx *gorm.DB) error { + var count int64 + if err := tx.Set("gorm:query_option", "FOR UPDATE").Model(&Server{}). + Where("sort = ? AND id != ?", m.Sort, m.Id).Count(&count).Error; err != nil { + return err + } + if count > 1 { + // reorder sort + if err := reorderSortWithServer(tx); err != nil { + logger.Errorf("[Server] BeforeUpdate reorderSort error: %v", err.Error()) + return err + } + // get max sort + var maxSort int + if err := tx.Model(&Server{}).Select("MAX(sort)").Scan(&maxSort).Error; err != nil { + return err + } + m.Sort = maxSort + 1 + } + return nil +} + +// MarshalProtocols Marshal server protocols to json +func (m *Server) MarshalProtocols(list []Protocol) error { + var validate = make(map[string]bool) + for _, protocol := range list { + if protocol.Type == "" { + return errors.New("protocol type is required") + } + if _, exists := validate[protocol.Type]; exists { + return errors.New("duplicate protocol type: " + protocol.Type) + } + validate[protocol.Type] = true + } + data, err := json.Marshal(list) + if err != nil { + return err + } + m.Protocols = string(data) + return nil +} + +// UnmarshalProtocols Unmarshal server protocols from json +func (m *Server) UnmarshalProtocols() ([]Protocol, error) { + var list []Protocol + if m.Protocols == "" { + return list, nil + } + err := json.Unmarshal([]byte(m.Protocols), &list) + if err != nil { + return nil, err + } + return list, nil +} + +type Protocol struct { + Type string `json:"type"` + Port uint16 `json:"port"` + Enable bool `json:"enable"` + Security string `json:"security,omitempty"` + SNI string `json:"sni,omitempty"` + AllowInsecure bool `json:"allow_insecure,omitempty"` + Fingerprint string `json:"fingerprint,omitempty"` + RealityServerAddr string `json:"reality_server_addr,omitempty"` + RealityServerPort int `json:"reality_server_port,omitempty"` + RealityPrivateKey string `json:"reality_private_key,omitempty"` + RealityPublicKey string `json:"reality_public_key,omitempty"` + RealityShortId string `json:"reality_short_id,omitempty"` + Transport string `json:"transport,omitempty"` + Host string `json:"host,omitempty"` + Path string `json:"path,omitempty"` + ServiceName string `json:"service_name,omitempty"` + Cipher string `json:"cipher,omitempty"` + ServerKey string `json:"server_key,omitempty"` + Flow string `json:"flow,omitempty"` + HopPorts string `json:"hop_ports,omitempty"` + HopInterval int `json:"hop_interval,omitempty"` + ObfsPassword string `json:"obfs_password,omitempty"` + DisableSNI bool `json:"disable_sni,omitempty"` + ReduceRtt bool `json:"reduce_rtt,omitempty"` + UDPRelayMode string `json:"udp_relay_mode,omitempty"` + CongestionController string `json:"congestion_controller,omitempty"` + Multiplex string `json:"multiplex,omitempty"` // mux, eg: off/low/medium/high + PaddingScheme string `json:"padding_scheme,omitempty"` // padding scheme + UpMbps int `json:"up_mbps,omitempty"` // upload speed limit + DownMbps int `json:"down_mbps,omitempty"` // download speed limit + Obfs string `json:"obfs,omitempty"` // obfs, 'none', 'http', 'tls' + ObfsHost string `json:"obfs_host,omitempty"` // obfs host + ObfsPath string `json:"obfs_path,omitempty"` // obfs path + XhttpMode string `json:"xhttp_mode,omitempty"` // xhttp mode + XhttpExtra string `json:"xhttp_extra,omitempty"` // xhttp extra path + Encryption string `json:"encryption,omitempty"` // encryption,'none', 'mlkem768x25519plus' + EncryptionMode string `json:"encryption_mode,omitempty"` // encryption mode,'native', 'xorpub', 'random' + EncryptionRtt string `json:"encryption_rtt,omitempty"` // encryption rtt,'0rtt', '1rtt' + EncryptionTicket string `json:"encryption_ticket,omitempty"` // encryption ticket + EncryptionServerPadding string `json:"encryption_server_padding,omitempty"` // encryption server padding + EncryptionPrivateKey string `json:"encryption_private_key,omitempty"` // encryption private key + EncryptionClientPadding string `json:"encryption_client_padding,omitempty"` // encryption client padding + EncryptionPassword string `json:"encryption_password,omitempty"` // encryption password + + Ratio float64 `json:"ratio,omitempty"` // Traffic ratio, default is 1 + CertMode string `json:"cert_mode,omitempty"` // Certificate mode, `none`|`http`|`dns`|`self` + CertDNSProvider string `json:"cert_dns_provider,omitempty"` // DNS provider for certificate + CertDNSEnv string `json:"cert_dns_env"` // Environment for DNS provider +} + +// Marshal protocol to json +func (m *Protocol) Marshal() ([]byte, error) { + type Alias Protocol + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(m), + }) +} + +// Unmarshal json to protocol +func (m *Protocol) Unmarshal(data []byte) error { + type Alias Protocol + aux := &struct { + *Alias + }{ + Alias: (*Alias)(m), + } + return json.Unmarshal(data, &aux) +} + +func reorderSortWithServer(tx *gorm.DB) error { + var servers []Server + if err := tx.Order("sort, id").Find(&servers).Error; err != nil { + return err + } + for i, server := range servers { + if server.Sort != i+1 { + if err := tx.Exec("UPDATE `servers` SET sort = ? WHERE id = ?", i+1, server.Id).Error; err != nil { + return err + } + } + } + return nil +} diff --git a/internal/model/order/model.go b/internal/model/order/model.go index a463aca..d98e361 100644 --- a/internal/model/order/model.go +++ b/internal/model/order/model.go @@ -40,6 +40,13 @@ type Details struct { UpdatedAt time.Time `gorm:"comment:Update Time"` } +type OrdersTotalWithDate struct { + Date string + AmountTotal int64 + NewOrderAmount int64 + RenewalOrderAmount int64 +} + type customOrderLogicModel interface { UpdateOrderStatus(ctx context.Context, orderNo string, status uint8, tx ...*gorm.DB) error QueryOrderListByPage(ctx context.Context, page, size int, status uint8, user, subscribe int64, search string) (int64, []*Details, error) @@ -52,6 +59,14 @@ type customOrderLogicModel interface { QueryDateUserCounts(ctx context.Context, date time.Time) (int64, int64, error) QueryTotalUserCounts(ctx context.Context) (int64, int64, error) IsUserEligibleForNewOrder(ctx context.Context, userID int64) (bool, error) + QueryDailyOrdersList(ctx context.Context, date time.Time) ([]OrdersTotalWithDate, error) + QueryMonthlyOrdersList(ctx context.Context, date time.Time) ([]OrdersTotalWithDate, error) +} + +// UserCounts User counts for new and renewal users +type UserCounts struct { + NewUsers int64 `gorm:"column:new_users"` + RenewalUsers int64 `gorm:"column:renewal_users"` } // NewModel returns a model for the database table. @@ -156,65 +171,78 @@ func (m *customOrderModel) QueryDateOrders(ctx context.Context, date time.Time) func (m *customOrderModel) QueryTotalOrders(ctx context.Context) (OrdersTotal, error) { var result OrdersTotal - err := m.QueryNoCacheCtx(ctx, &result, func(conn *gorm.DB, v interface{}) error { + + err := m.QueryNoCacheCtx(ctx, &result, func(conn *gorm.DB, _ interface{}) error { return conn.Model(&Order{}). + Select(` + SUM(amount) AS amount_total, + SUM(CASE WHEN is_new = 1 THEN amount ELSE 0 END) AS new_order_amount, + SUM(CASE WHEN is_new = 0 THEN amount ELSE 0 END) AS renewal_order_amount + `). Where("status IN ? AND method != ?", []int64{2, 5}, "balance"). - Select( - "SUM(amount) as amount_total, " + - "SUM(CASE WHEN is_new = 1 THEN amount ELSE 0 END) as new_order_amount, " + - "SUM(CASE WHEN is_new = 0 THEN amount ELSE 0 END) as renewal_order_amount", - ). - Scan(v).Error + Scan(&result).Error }) + return result, err } func (m *customOrderModel) QueryMonthlyUserCounts(ctx context.Context, date time.Time) (int64, int64, error) { + // 获取当月第一天零点 firstDay := time.Date(date.Year(), date.Month(), 1, 0, 0, 0, 0, date.Location()) - lastDay := firstDay.AddDate(0, 1, -1) + // 获取下个月第一天零点(避免漏掉最后一天的订单) + nextMonth := firstDay.AddDate(0, 1, 0) - var newUsers int64 - var renewalUsers int64 + var counts UserCounts + + // 执行查询 err := m.QueryNoCacheCtx(ctx, nil, func(conn *gorm.DB, _ interface{}) error { return conn.Model(&Order{}). - Where("status IN ? AND created_at BETWEEN ? AND ? AND method != ?", []int64{2, 5}, firstDay, lastDay, "balance"). - Select( - "COUNT(DISTINCT CASE WHEN is_new = 1 THEN user_id END) as new_users, "+ - "COUNT(DISTINCT CASE WHEN is_new = 0 THEN user_id END) as renewal_users"). - Row().Scan(&newUsers, &renewalUsers) + Select(` + COUNT(DISTINCT CASE WHEN is_new = 1 THEN user_id END) AS new_users, + COUNT(DISTINCT CASE WHEN is_new = 0 THEN user_id END) AS renewal_users + `). + Where("status IN ? AND created_at >= ? AND created_at < ? AND method != ?", + []int64{2, 5}, firstDay, nextMonth, "balance"). + Scan(&counts).Error }) - return newUsers, renewalUsers, err -} + return counts.NewUsers, counts.RenewalUsers, err +} func (m *customOrderModel) QueryDateUserCounts(ctx context.Context, date time.Time) (int64, int64, error) { - start := date.Truncate(24 * time.Hour) - end := start.Add(24 * time.Hour).Add(-time.Nanosecond) + // 当天 00:00:00 + start := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location()) + // 下一天 00:00:00 + nextDay := start.Add(24 * time.Hour) + + var counts UserCounts - var newUsers int64 - var renewalUsers int64 err := m.QueryNoCacheCtx(ctx, nil, func(conn *gorm.DB, _ interface{}) error { return conn.Model(&Order{}). - Where("status IN ? AND created_at BETWEEN ? AND ? AND method != ?", []int64{2, 5}, start, end, "balance"). - Select( - "COUNT(DISTINCT CASE WHEN is_new = 1 THEN user_id END) as new_users, "+ - "COUNT(DISTINCT CASE WHEN is_new = 0 THEN user_id END) as renewal_users"). - Row().Scan(&newUsers, &renewalUsers) + Select(` + COUNT(DISTINCT CASE WHEN is_new = 1 THEN user_id END) AS new_users, + COUNT(DISTINCT CASE WHEN is_new = 0 THEN user_id END) AS renewal_users + `). + Where("status IN ? AND created_at >= ? AND created_at < ? AND method != ?", + []int64{2, 5}, start, nextDay, "balance"). + Scan(&counts).Error }) - return newUsers, renewalUsers, err -} + return counts.NewUsers, counts.RenewalUsers, err +} func (m *customOrderModel) QueryTotalUserCounts(ctx context.Context) (int64, int64, error) { - var newUsers int64 - var renewalUsers int64 + var counts UserCounts + err := m.QueryNoCacheCtx(ctx, nil, func(conn *gorm.DB, _ interface{}) error { return conn.Model(&Order{}). Where("status IN ? AND method != ?", []int64{2, 5}, "balance"). - Select( - "COUNT(DISTINCT CASE WHEN is_new = 1 THEN user_id END) as new_users, "+ - "COUNT(DISTINCT CASE WHEN is_new = 0 THEN user_id END) as renewal_users"). - Row().Scan(&newUsers, &renewalUsers) + Select(` + COUNT(DISTINCT CASE WHEN is_new = 1 THEN user_id END) AS new_users, + COUNT(DISTINCT CASE WHEN is_new = 0 THEN user_id END) AS renewal_users + `). + Scan(&counts).Error }) - return newUsers, renewalUsers, err + + return counts.NewUsers, counts.RenewalUsers, err } func (m *customOrderModel) IsUserEligibleForNewOrder(ctx context.Context, userID int64) (bool, error) { @@ -226,3 +254,55 @@ func (m *customOrderModel) IsUserEligibleForNewOrder(ctx context.Context, userID }) return count == 0, err } + +// QueryDailyOrdersList 查询当月每日订单统计 +func (m *customOrderModel) QueryDailyOrdersList(ctx context.Context, date time.Time) ([]OrdersTotalWithDate, error) { + var results []OrdersTotalWithDate + + err := m.QueryNoCacheCtx(ctx, &results, func(conn *gorm.DB, v interface{}) error { + // 当月 1 号 00:00:00 + firstDay := time.Date(date.Year(), date.Month(), 1, 0, 0, 0, 0, date.Location()) + // 第二天 00:00:00 + nextDay := date.AddDate(0, 0, 1).Truncate(24 * time.Hour) + + return conn.Model(&Order{}). + Select(` + DATE_FORMAT(created_at, '%Y-%m-%d') AS date, + SUM(amount) AS amount_total, + SUM(CASE WHEN is_new = 1 THEN amount ELSE 0 END) AS new_order_amount, + SUM(CASE WHEN is_new = 0 THEN amount ELSE 0 END) AS renewal_order_amount + `). + Where("status IN ? AND created_at >= ? AND created_at < ? AND method != ?", + []int64{2, 5}, firstDay, nextDay, "balance"). + Group("DATE_FORMAT(created_at, '%Y-%m-%d')"). + Order("date ASC"). + Scan(v).Error + }) + return results, err +} + +// QueryMonthlyOrdersList 查询过去 6 个月订单统计(包含当前月) +func (m *customOrderModel) QueryMonthlyOrdersList(ctx context.Context, date time.Time) ([]OrdersTotalWithDate, error) { + var results []OrdersTotalWithDate + + err := m.QueryNoCacheCtx(ctx, &results, func(conn *gorm.DB, v interface{}) error { + // 六个月前(取月初) + start := time.Date(date.Year(), date.Month(), 1, 0, 0, 0, 0, date.Location()).AddDate(0, -5, 0) + // 下个月月初 + end := time.Date(date.Year(), date.Month(), 1, 0, 0, 0, 0, date.Location()).AddDate(0, 1, 0) + + return conn.Model(&Order{}). + Select(` + DATE_FORMAT(created_at, '%Y-%m') AS date, + SUM(amount) AS amount_total, + SUM(CASE WHEN is_new = 1 THEN amount ELSE 0 END) AS new_order_amount, + SUM(CASE WHEN is_new = 0 THEN amount ELSE 0 END) AS renewal_order_amount + `). + Where("status IN ? AND created_at >= ? AND created_at < ? AND method != ?", + []int64{2, 5}, start, end, "balance"). + Group("DATE_FORMAT(created_at, '%Y-%m')"). + Order("date ASC"). + Scan(v).Error + }) + return results, err +} diff --git a/internal/model/payment/payment.go b/internal/model/payment/payment.go index b76a61f..ad2f046 100644 --- a/internal/model/payment/payment.go +++ b/internal/model/payment/payment.go @@ -46,13 +46,19 @@ type StripeConfig struct { Payment string `json:"payment"` } -func (l *StripeConfig) Marshal() string { - b, _ := json.Marshal(l) - return string(b) +func (l *StripeConfig) Marshal() ([]byte, error) { + type Alias StripeConfig + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(l), + }) } -func (l *StripeConfig) Unmarshal(s string) error { - return json.Unmarshal([]byte(s), l) +func (l *StripeConfig) Unmarshal(data []byte) error { + type Alias StripeConfig + aux := (*Alias)(l) + return json.Unmarshal(data, &aux) } type AlipayF2FConfig struct { @@ -63,26 +69,61 @@ type AlipayF2FConfig struct { Sandbox bool `json:"sandbox"` } -func (l *AlipayF2FConfig) Marshal() string { - b, _ := json.Marshal(l) - return string(b) +func (l *AlipayF2FConfig) Marshal() ([]byte, error) { + type Alias AlipayF2FConfig + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(l), + }) } -func (l *AlipayF2FConfig) Unmarshal(s string) error { - return json.Unmarshal([]byte(s), l) +func (l *AlipayF2FConfig) Unmarshal(data []byte) error { + type Alias AlipayF2FConfig + aux := (*Alias)(l) + return json.Unmarshal(data, &aux) } type EPayConfig struct { - Pid string `json:"pid"` - Url string `json:"url"` - Key string `json:"key"` + Pid string `json:"pid"` + Url string `json:"url"` + Key string `json:"key"` + Type string `json:"type"` } -func (l *EPayConfig) Marshal() string { - b, _ := json.Marshal(l) - return string(b) +func (l *EPayConfig) Marshal() ([]byte, error) { + type Alias EPayConfig + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(l), + }) } -func (l *EPayConfig) Unmarshal(s string) error { - return json.Unmarshal([]byte(s), l) +func (l *EPayConfig) Unmarshal(data []byte) error { + type Alias EPayConfig + aux := (*Alias)(l) + return json.Unmarshal(data, &aux) +} + +type CryptoSaaSConfig struct { + Endpoint string `json:"endpoint"` + AccountID string `json:"account_id"` + SecretKey string `json:"secret_key"` + Type string `json:"type"` +} + +func (l *CryptoSaaSConfig) Marshal() ([]byte, error) { + type Alias CryptoSaaSConfig + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(l), + }) +} + +func (l *CryptoSaaSConfig) Unmarshal(data []byte) error { + type Alias CryptoSaaSConfig + aux := (*Alias)(l) + return json.Unmarshal(data, &aux) } diff --git a/internal/model/server/default.go b/internal/model/server/default.go index eecf193..b013f1a 100644 --- a/internal/model/server/default.go +++ b/internal/model/server/default.go @@ -5,8 +5,6 @@ import ( "errors" "fmt" - "github.com/perfect-panel/server/internal/config" - "github.com/perfect-panel/server/pkg/cache" "github.com/redis/go-redis/v9" "gorm.io/gorm" @@ -62,17 +60,23 @@ func (m *defaultServerModel) batchGetCacheKeys(Servers ...*Server) []string { return keys } + func (m *defaultServerModel) getCacheKeys(data *Server) []string { if data == nil { return []string{} } detailsKey := fmt.Sprintf("%s%v", CacheServerDetailPrefix, data.Id) ServerIdKey := fmt.Sprintf("%s%v", cacheServerIdPrefix, data.Id) - configIdKey := fmt.Sprintf("%s%v", config.ServerConfigCacheKey, data.Id) + //configIdKey := fmt.Sprintf("%s%v", config.ServerConfigCacheKey, data.Id) + //userIDKey := fmt.Sprintf("%s%d", config.ServerUserListCacheKey, data.Id) + + // query protocols to get config keys + cacheKeys := []string{ ServerIdKey, detailsKey, - configIdKey, + //configIdKey, + //userIDKey, } return cacheKeys } diff --git a/internal/model/server/model.go b/internal/model/server/model.go index daace5f..58ae7b7 100644 --- a/internal/model/server/model.go +++ b/internal/model/server/model.go @@ -5,7 +5,6 @@ import ( "fmt" "strings" - "github.com/perfect-panel/server/internal/config" "gorm.io/gorm" ) @@ -45,9 +44,10 @@ var ( // ClearCache Clear Cache func (m *customServerModel) ClearCache(ctx context.Context, id int64) error { serverIdKey := fmt.Sprintf("%s%v", cacheServerIdPrefix, id) - configKey := fmt.Sprintf("%s%d", config.ServerConfigCacheKey, id) + //configKey := fmt.Sprintf("%s%d", config.ServerConfigCacheKey, id) + //userListKey := fmt.Sprintf("%s%v", config.ServerUserListCacheKey, id) - return m.DelCacheCtx(ctx, serverIdKey, configKey) + return m.DelCacheCtx(ctx, serverIdKey) } // QueryServerCountByServerGroups Query Server Count By Server Groups diff --git a/internal/model/server/server.go b/internal/model/server/server.go index c185503..2da10da 100644 --- a/internal/model/server/server.go +++ b/internal/model/server/server.go @@ -26,6 +26,7 @@ type ServerFilter struct { Size int } +// Deprecated: use internal/model/node/server.go type Server struct { Id int64 `gorm:"primary_key"` Name string `gorm:"type:varchar(100);not null;default:'';comment:Node Name"` @@ -138,6 +139,15 @@ type Hysteria2 struct { } type Tuic struct { + Port int `json:"port"` + DisableSNI bool `json:"disable_sni"` + ReduceRtt bool `json:"reduce_rtt"` + UDPRelayMode string `json:"udp_relay_mode"` + CongestionController string `json:"congestion_controller"` + SecurityConfig SecurityConfig `json:"security_config"` +} + +type AnyTLS struct { Port int `json:"port"` SecurityConfig SecurityConfig `json:"security_config"` } diff --git a/internal/model/subscribe/default.go b/internal/model/subscribe/default.go index 2bdc1fa..29e748c 100644 --- a/internal/model/subscribe/default.go +++ b/internal/model/subscribe/default.go @@ -4,8 +4,11 @@ import ( "context" "errors" "fmt" + "strings" + "github.com/perfect-panel/server/internal/model/node" "github.com/perfect-panel/server/pkg/cache" + "github.com/perfect-panel/server/pkg/tool" "github.com/redis/go-redis/v9" "gorm.io/gorm" ) @@ -57,11 +60,34 @@ func (m *defaultSubscribeModel) getCacheKeys(data *Subscribe) []string { if data == nil { return []string{} } - SubscribeIdKey := fmt.Sprintf("%s%v", cacheSubscribeIdPrefix, data.Id) - cacheKeys := []string{ - SubscribeIdKey, + var keys []string + if data.Nodes != "" { + var nodes []*node.Node + ids := strings.Split(data.Nodes, ",") + + err := m.QueryNoCacheCtx(context.Background(), &nodes, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&node.Node{}).Where("id IN (?)", tool.StringSliceToInt64Slice(ids)).Find(&nodes).Error + }) + if err == nil { + for _, n := range nodes { + keys = append(keys, fmt.Sprintf("%s%d", node.ServerUserListCacheKey, n.ServerId)) + } + } } - return cacheKeys + if data.NodeTags != "" { + var nodes []*node.Node + tags := tool.RemoveDuplicateElements(strings.Split(data.NodeTags, ",")...) + err := m.QueryNoCacheCtx(context.Background(), &nodes, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&node.Node{}).Scopes(InSet("tags", tags)).Find(&nodes).Error + }) + if err == nil { + for _, n := range nodes { + keys = append(keys, fmt.Sprintf("%s%d", node.ServerUserListCacheKey, n.ServerId)) + } + } + } + + return append(keys, fmt.Sprintf("%s%v", cacheSubscribeIdPrefix, data.Id)) } func (m *defaultSubscribeModel) Insert(ctx context.Context, data *Subscribe, tx ...*gorm.DB) error { diff --git a/internal/model/subscribe/model.go b/internal/model/subscribe/model.go index f3b1142..9942046 100644 --- a/internal/model/subscribe/model.go +++ b/internal/model/subscribe/model.go @@ -3,38 +3,37 @@ package subscribe import ( "context" + "github.com/perfect-panel/server/pkg/tool" "github.com/redis/go-redis/v9" "gorm.io/gorm" ) -// type Details struct { -// Id int64 `gorm:"primaryKey"` -// Name string `gorm:"type:varchar(255);not null;default:'';comment:Subscribe Name"` -// Description string `gorm:"type:text;comment:Subscribe Description"` -// UnitPrice int64 `gorm:"type:int;not null;default:0;comment:Unit Price"` -// UnitTime string `gorm:"type:varchar(255);not null;default:'';comment:Unit Time"` -// Discount string `gorm:"type:text;comment:Discount"` -// Replacement int64 `gorm:"type:int;not null;default:0;comment:Replacement"` -// Inventory int64 `gorm:"type:int;not null;default:0;comment:Inventory"` -// Traffic int64 `gorm:"type:int;not null;default:0;comment:Traffic"` -// SpeedLimit int64 `gorm:"type:int;not null;default:0;comment:Speed Limit"` -// DeviceLimit int64 `gorm:"type:int;not null;default:0;comment:Device Limit"` -// GroupId int64 `gorm:"type:bigint;comment:Group Id"` -// Quota int64 `gorm:"type:int;not null;default:0;comment:Quota"` -// Show *bool `gorm:"type:tinyint(1);not null;default:0;comment:Show"` -// Sell *bool `gorm:"type:tinyint(1);not null;default:0;comment:Sell"` -// DeductionRatio int64 `gorm:"type:int;default:0;comment:Deduction Ratio"` -// PurchaseWithDiscount bool `gorm:"type:tinyint(1);default:0;comment:PurchaseWithDiscount"` -// ResetCycle int64 `gorm:"type:int;default:0;comment:Reset Cycle"` -// RenewalReset bool `gorm:"type:tinyint(1);default:0;comment:Renew Reset"` -// } +type FilterParams struct { + Page int // Page Number + Size int // Page Size + Ids []int64 // Subscribe IDs + Node []int64 // Node IDs + Tags []string // Node Tags + Show bool // Show Portal Page + Sell bool // Sell + Language string // Language + DefaultLanguage bool // Default Subscribe Language Data + Search string // Search Keywords +} + +func (p *FilterParams) Normalize() { + if p.Page <= 0 { + p.Page = 1 + } + if p.Size <= 0 { + p.Size = 10 + } +} + type customSubscribeLogicModel interface { - QuerySubscribeListByPage(ctx context.Context, page, size int, group int64, search string) (total int64, list []*Subscribe, err error) - QuerySubscribeList(ctx context.Context) ([]*Subscribe, error) - QuerySubscribeListByShow(ctx context.Context) ([]*Subscribe, error) - QuerySubscribeIdsByServerIdAndServerGroupId(ctx context.Context, serverId, serverGroupId int64) ([]*Subscribe, error) + FilterList(ctx context.Context, params *FilterParams) (int64, []*Subscribe, error) + ClearCache(ctx context.Context, id ...int64) error QuerySubscribeMinSortByIds(ctx context.Context, ids []int64) (int64, error) - QuerySubscribeListByIds(ctx context.Context, ids []int64) ([]*Subscribe, error) } // NewModel returns a model for the database table. @@ -44,54 +43,6 @@ func NewModel(conn *gorm.DB, c *redis.Client) Model { } } -// QuerySubscribeListByPage Get Subscribe List -func (m *customSubscribeModel) QuerySubscribeListByPage(ctx context.Context, page, size int, group int64, search string) (total int64, list []*Subscribe, err error) { - err = m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { - // About to be abandoned - _ = conn.Model(&Subscribe{}). - Where("sort = ?", 0). - Update("sort", gorm.Expr("id")) - - conn = conn.Model(&Subscribe{}) - if group > 0 { - conn = conn.Where("group_id = ?", group) - } - if search != "" { - conn = conn.Where("`name` like ? or `description` like ?", "%"+search+"%", "%"+search+"%") - } - return conn.Count(&total).Order("sort ASC").Limit(size).Offset((page - 1) * size).Find(v).Error - }) - return total, list, err -} - -// QuerySubscribeList Get Subscribe List -func (m *customSubscribeModel) QuerySubscribeList(ctx context.Context) ([]*Subscribe, error) { - var list []*Subscribe - err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { - conn = conn.Model(&Subscribe{}) - return conn.Where("`sell` = true").Order("sort ").Find(v).Error - }) - return list, err -} - -func (m *customSubscribeModel) QuerySubscribeIdsByServerIdAndServerGroupId(ctx context.Context, serverId, serverGroupId int64) ([]*Subscribe, error) { - var data []*Subscribe - err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error { - return conn.Model(&Subscribe{}).Where("FIND_IN_SET(?, server)", serverId).Or("FIND_IN_SET(?, server_group)", serverGroupId).Find(v).Error - }) - return data, err -} - -// QuerySubscribeListByShow Get Subscribe List By Show -func (m *customSubscribeModel) QuerySubscribeListByShow(ctx context.Context) ([]*Subscribe, error) { - var list []*Subscribe - err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { - conn = conn.Model(&Subscribe{}) - return conn.Where("`show` = true").Find(v).Error - }) - return list, err -} - func (m *customSubscribeModel) QuerySubscribeMinSortByIds(ctx context.Context, ids []int64) (int64, error) { var minSort int64 err := m.QueryNoCacheCtx(ctx, &minSort, func(conn *gorm.DB, v interface{}) error { @@ -100,10 +51,106 @@ func (m *customSubscribeModel) QuerySubscribeMinSortByIds(ctx context.Context, i return minSort, err } -func (m *customSubscribeModel) QuerySubscribeListByIds(ctx context.Context, ids []int64) ([]*Subscribe, error) { - var list []*Subscribe - err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { - return conn.Model(&Subscribe{}).Where("id IN ?", ids).Find(v).Error - }) - return list, err +func (m *customSubscribeModel) ClearCache(ctx context.Context, ids ...int64) error { + if len(ids) <= 0 { + return nil + } + + var cacheKeys []string + for _, id := range ids { + data, err := m.FindOne(ctx, id) + if err != nil { + return err + } + cacheKeys = append(cacheKeys, m.getCacheKeys(data)...) + } + return m.CachedConn.DelCacheCtx(ctx, cacheKeys...) +} + +// FilterList Filter Subscribe List +func (m *customSubscribeModel) FilterList(ctx context.Context, params *FilterParams) (int64, []*Subscribe, error) { + if params == nil { + params = &FilterParams{} + } + params.Normalize() + + var list []*Subscribe + var total int64 + + // 构建查询函数 + buildQuery := func(conn *gorm.DB, lang string) *gorm.DB { + query := conn.Model(&Subscribe{}) + + if params.Search != "" { + s := "%" + params.Search + "%" + query = query.Where("`name` LIKE ? OR `description` LIKE ?", s, s) + } + if params.Show { + query = query.Where("`show` = true") + } + if params.Sell { + query = query.Where("`sell` = true") + } + + if len(params.Ids) > 0 { + query = query.Where("id IN ?", params.Ids) + } + if len(params.Node) > 0 { + query = query.Scopes(InSet("nodes", tool.Int64SliceToStringSlice(params.Node))) + } + + if len(params.Tags) > 0 { + query = query.Scopes(InSet("node_tags", params.Tags)) + } + if lang != "" { + query = query.Where("language = ?", lang) + } else if params.DefaultLanguage { + query = query.Where("language = ''") + } + + return query + } + + // 查询数据 + queryFunc := func(lang string) error { + return m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { + query := buildQuery(conn, lang) + if err := query.Count(&total).Error; err != nil { + return err + } + return query.Order("sort ASC"). + Limit(params.Size). + Offset((params.Page - 1) * params.Size). + Find(v).Error + }) + } + + err := queryFunc(params.Language) + if err != nil { + return 0, nil, err + } + + // fallback 默认语言 + if params.DefaultLanguage && total == 0 { + err = queryFunc("") + if err != nil { + return 0, nil, err + } + } + + return total, list, nil +} + +func InSet(field string, values []string) func(db *gorm.DB) *gorm.DB { + return func(db *gorm.DB) *gorm.DB { + if len(values) == 0 { + return db + } + + query := db.Where("1=0") + for _, v := range values { + query = query.Or("FIND_IN_SET(?, "+field+")", v) + } + return query + } } diff --git a/internal/model/subscribe/subscribe.go b/internal/model/subscribe/subscribe.go index 4261715..a80ea63 100644 --- a/internal/model/subscribe/subscribe.go +++ b/internal/model/subscribe/subscribe.go @@ -9,6 +9,7 @@ import ( type Subscribe struct { Id int64 `gorm:"primaryKey"` Name string `gorm:"type:varchar(255);not null;default:'';comment:Subscribe Name"` + Language string `gorm:"type:varchar(255);not null;default:'';comment:Language"` Description string `gorm:"type:text;comment:Subscribe Description"` UnitPrice int64 `gorm:"type:int;not null;default:0;comment:Unit Price"` UnitTime string `gorm:"type:varchar(255);not null;default:'';comment:Unit Time"` @@ -19,9 +20,8 @@ type Subscribe struct { SpeedLimit int64 `gorm:"type:int;not null;default:0;comment:Speed Limit"` DeviceLimit int64 `gorm:"type:int;not null;default:0;comment:Device Limit"` Quota int64 `gorm:"type:int;not null;default:0;comment:Quota"` - GroupId int64 `gorm:"type:bigint;comment:Group Id"` - ServerGroup string `gorm:"type:varchar(255);comment:Server Group"` - Server string `gorm:"type:varchar(255);comment:Server"` + Nodes string `gorm:"type:varchar(255);comment:Node Ids"` + NodeTags string `gorm:"type:varchar(255);comment:Node Tags"` Show *bool `gorm:"type:tinyint(1);not null;default:0;comment:Show portal page"` Sell *bool `gorm:"type:tinyint(1);not null;default:0;comment:Sell"` Sort int64 `gorm:"type:int;not null;default:0;comment:Sort"` diff --git a/internal/model/subscribeType/default.go b/internal/model/subscribeType/default.go deleted file mode 100644 index cf6d8bd..0000000 --- a/internal/model/subscribeType/default.go +++ /dev/null @@ -1,117 +0,0 @@ -package subscribeType - -import ( - "context" - "errors" - "fmt" - - "github.com/perfect-panel/server/pkg/cache" - "github.com/redis/go-redis/v9" - "gorm.io/gorm" -) - -var _ Model = (*customSubscribeTypeModel)(nil) -var ( - cacheSubscribeTypeIdPrefix = "cache:subscribeType:id:" -) - -type ( - Model interface { - subscribeTypeModel - customSubscribeTypeLogicModel - } - subscribeTypeModel interface { - Insert(ctx context.Context, data *SubscribeType) error - FindOne(ctx context.Context, id int64) (*SubscribeType, error) - Update(ctx context.Context, data *SubscribeType) error - Delete(ctx context.Context, id int64) error - Transaction(ctx context.Context, fn func(db *gorm.DB) error) error - } - - customSubscribeTypeModel struct { - *defaultSubscribeTypeModel - } - defaultSubscribeTypeModel struct { - cache.CachedConn - table string - } -) - -func newSubscribeTypeModel(db *gorm.DB, c *redis.Client) *defaultSubscribeTypeModel { - return &defaultSubscribeTypeModel{ - CachedConn: cache.NewConn(db, c), - table: "`SubscribeType`", - } -} - -//nolint:unused -func (m *defaultSubscribeTypeModel) batchGetCacheKeys(SubscribeTypes ...*SubscribeType) []string { - var keys []string - for _, subscribeType := range SubscribeTypes { - keys = append(keys, m.getCacheKeys(subscribeType)...) - } - return keys - -} -func (m *defaultSubscribeTypeModel) getCacheKeys(data *SubscribeType) []string { - if data == nil { - return []string{} - } - SubscribeTypeIdKey := fmt.Sprintf("%s%v", cacheSubscribeTypeIdPrefix, data.Id) - cacheKeys := []string{ - SubscribeTypeIdKey, - } - return cacheKeys -} - -func (m *defaultSubscribeTypeModel) Insert(ctx context.Context, data *SubscribeType) error { - err := m.ExecCtx(ctx, func(conn *gorm.DB) error { - return conn.Create(&data).Error - }, m.getCacheKeys(data)...) - return err -} - -func (m *defaultSubscribeTypeModel) FindOne(ctx context.Context, id int64) (*SubscribeType, error) { - SubscribeTypeIdKey := fmt.Sprintf("%s%v", cacheSubscribeTypeIdPrefix, id) - var resp SubscribeType - err := m.QueryCtx(ctx, &resp, SubscribeTypeIdKey, func(conn *gorm.DB, v interface{}) error { - return conn.Model(&SubscribeType{}).Where("`id` = ?", id).First(&resp).Error - }) - switch { - case err == nil: - return &resp, nil - default: - return nil, err - } -} - -func (m *defaultSubscribeTypeModel) Update(ctx context.Context, data *SubscribeType) error { - old, err := m.FindOne(ctx, data.Id) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return err - } - err = m.ExecCtx(ctx, func(conn *gorm.DB) error { - db := conn - return db.Save(data).Error - }, m.getCacheKeys(old)...) - return err -} - -func (m *defaultSubscribeTypeModel) Delete(ctx context.Context, id int64) error { - data, err := m.FindOne(ctx, id) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil - } - return err - } - err = m.ExecCtx(ctx, func(conn *gorm.DB) error { - db := conn - return db.Delete(&SubscribeType{}, id).Error - }, m.getCacheKeys(data)...) - return err -} - -func (m *defaultSubscribeTypeModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error { - return m.TransactCtx(ctx, fn) -} diff --git a/internal/model/subscribeType/model.go b/internal/model/subscribeType/model.go deleted file mode 100644 index 52e7e0f..0000000 --- a/internal/model/subscribeType/model.go +++ /dev/null @@ -1,16 +0,0 @@ -package subscribeType - -import ( - "github.com/redis/go-redis/v9" - "gorm.io/gorm" -) - -type customSubscribeTypeLogicModel interface { -} - -// NewModel returns a model for the database table. -func NewModel(conn *gorm.DB, c *redis.Client) Model { - return &customSubscribeTypeModel{ - defaultSubscribeTypeModel: newSubscribeTypeModel(conn, c), - } -} diff --git a/internal/model/subscribeType/subscribeType.go b/internal/model/subscribeType/subscribeType.go deleted file mode 100644 index c0a9d94..0000000 --- a/internal/model/subscribeType/subscribeType.go +++ /dev/null @@ -1,15 +0,0 @@ -package subscribeType - -import "time" - -type SubscribeType struct { - Id int64 `gorm:"primary_key"` - Name string `gorm:"type:varchar(50);default:'';not null;comment:订阅类型"` - Mark string `gorm:"type:varchar(255);default:'';not null;comment:订阅标识"` - CreatedAt time.Time `gorm:"<-:create;comment:创建时间"` - UpdatedAt time.Time `gorm:"comment:更新时间"` -} - -func (SubscribeType) TableName() string { - return "subscribe_type" -} diff --git a/internal/model/system/model.go b/internal/model/system/model.go index 60cef38..a7c28e1 100644 --- a/internal/model/system/model.go +++ b/internal/model/system/model.go @@ -19,6 +19,7 @@ type customSystemLogicModel interface { GetTosConfig(ctx context.Context) ([]*System, error) GetCurrencyConfig(ctx context.Context) ([]*System, error) GetVerifyCodeConfig(ctx context.Context) ([]*System, error) + GetLogConfig(ctx context.Context) ([]*System, error) UpdateNodeMultiplierConfig(ctx context.Context, config string) error FindNodeMultiplierConfig(ctx context.Context) (*System, error) } @@ -152,3 +153,12 @@ func (m *customSystemModel) GetVerifyCodeConfig(ctx context.Context) ([]*System, }) return configs, err } + +// GetLogConfig returns the log config. +func (m *customSystemModel) GetLogConfig(ctx context.Context) ([]*System, error) { + var configs []*System + err := m.QueryNoCacheCtx(ctx, &configs, func(conn *gorm.DB, v interface{}) error { + return conn.Where("`category` = ?", "log").Find(v).Error + }) + return configs, err +} diff --git a/internal/model/task/task.go b/internal/model/task/task.go new file mode 100644 index 0000000..5c1b987 --- /dev/null +++ b/internal/model/task/task.go @@ -0,0 +1,151 @@ +package task + +import ( + "encoding/json" + "time" +) + +type Type int8 + +const ( + Undefined Type = -1 + TypeEmail = iota + TypeQuota +) + +type Task struct { + Id int64 `gorm:"primaryKey;autoIncrement;comment:ID"` + Type int8 `gorm:"not null;comment:Task Type"` + Scope string `gorm:"type:text;comment:Task Scope"` + Content string `gorm:"type:text;comment:Task Content"` + Status int8 `gorm:"not null;default:0;comment:Task Status: 0: Pending, 1: In Progress, 2: Completed, 3: Failed"` + Errors string `gorm:"type:text;comment:Task Errors"` + Total uint64 `gorm:"column:total;not null;default:0;comment:Total Number"` + Current uint64 `gorm:"column:current;not null;default:0;comment:Current Number"` + CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` +} + +func (Task) TableName() string { + return "task" +} + +type ScopeType int8 + +const ( + ScopeAll ScopeType = iota + 1 // All users + ScopeActive // Active users + ScopeExpired // Expired users + ScopeNone // No Subscribe + ScopeSkip // Skip user filtering +) + +func (t ScopeType) Int8() int8 { + return int8(t) +} + +type EmailScope struct { + Type int8 `gorm:"not null;comment:Scope Type"` + RegisterStartTime int64 `json:"register_start_time"` + RegisterEndTime int64 `json:"register_end_time"` + Recipients []string `json:"recipients"` // list of email addresses + Additional []string `json:"additional"` // additional email addresses + Scheduled int64 `json:"scheduled"` // scheduled time (unix timestamp) + Interval uint8 `json:"interval"` // interval in seconds + Limit uint64 `json:"limit"` // daily send limit +} + +func (s *EmailScope) Marshal() ([]byte, error) { + type Alias EmailScope + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(s), + }) +} + +func (s *EmailScope) Unmarshal(data []byte) error { + type Alias EmailScope + aux := (*Alias)(s) + return json.Unmarshal(data, &aux) +} + +type EmailContent struct { + Subject string `json:"subject"` + Content string `json:"content"` +} + +func (c *EmailContent) Marshal() ([]byte, error) { + type Alias EmailContent + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(c), + }) +} + +func (c *EmailContent) Unmarshal(data []byte) error { + type Alias EmailContent + aux := (*Alias)(c) + return json.Unmarshal(data, &aux) +} + +type QuotaScope struct { + Subscribers []int64 `json:"subscribers"` // Subscribe IDs + IsActive *bool `json:"is_active"` // filter by active status + StartTime int64 `json:"start_time"` // filter by subscription start time + EndTime int64 `json:"end_time"` // filter by subscription end time + Objects []int64 `json:"recipients"` // list of user subs IDs +} + +func (s *QuotaScope) Marshal() ([]byte, error) { + type Alias QuotaScope + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(s), + }) +} + +func (s *QuotaScope) Unmarshal(data []byte) error { + type Alias QuotaScope + aux := (*Alias)(s) + return json.Unmarshal(data, &aux) +} + +type QuotaContent struct { + ResetTraffic bool `json:"reset_traffic"` // whether to reset traffic + Days uint64 `json:"days,omitempty"` // days to add + GiftType uint8 `json:"gift_type,omitempty"` // 1: Fixed, 2: Ratio + GiftValue uint64 `json:"gift_value,omitempty"` // value of the gift type +} + +func (c *QuotaContent) Marshal() ([]byte, error) { + type Alias QuotaContent + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(c), + }) +} + +func (c *QuotaContent) Unmarshal(data []byte) error { + type Alias QuotaContent + aux := (*Alias)(c) + return json.Unmarshal(data, &aux) +} + +func ParseScopeType(t int8) ScopeType { + switch t { + case 1: + return ScopeAll + case 2: + return ScopeActive + case 3: + return ScopeExpired + case 4: + return ScopeNone + default: + return ScopeSkip + } +} diff --git a/internal/model/user/authMethod.go b/internal/model/user/authMethod.go index 07faabd..18ce951 100644 --- a/internal/model/user/authMethod.go +++ b/internal/model/user/authMethod.go @@ -3,6 +3,7 @@ package user import ( "context" + "github.com/perfect-panel/server/pkg/logger" "gorm.io/gorm" ) @@ -31,24 +32,50 @@ func (m *defaultUserModel) FindUserAuthMethodByPlatform(ctx context.Context, use } func (m *defaultUserModel) InsertUserAuthMethods(ctx context.Context, data *AuthMethods, tx ...*gorm.DB) error { + u, err := m.FindOne(ctx, data.UserId) + if err != nil { + return err + } + return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { if len(tx) > 0 { conn = tx[0] } - return conn.Model(&AuthMethods{}).Create(data).Error + if err = conn.Model(&AuthMethods{}).Create(data).Error; err != nil { + return err + } + return m.ClearUserCache(ctx, u) }) } func (m *defaultUserModel) UpdateUserAuthMethods(ctx context.Context, data *AuthMethods, tx ...*gorm.DB) error { + u, err := m.FindOne(ctx, data.UserId) + if err != nil { + return err + } + return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { if len(tx) > 0 { conn = tx[0] } - return conn.Model(&AuthMethods{}).Where("user_id = ? AND auth_type = ?", data.UserId, data.AuthType).Save(data).Error + err = conn.Model(&AuthMethods{}).Where("user_id = ? AND auth_type = ?", data.UserId, data.AuthType).Save(data).Error + if err != nil { + return err + } + return m.ClearUserCache(ctx, u) }) } func (m *defaultUserModel) DeleteUserAuthMethods(ctx context.Context, userId int64, platform string, tx ...*gorm.DB) error { + u, err := m.FindOne(ctx, userId) + if err != nil { + return err + } + defer func() { + if err = m.ClearUserCache(context.Background(), u); err != nil { + logger.Errorf("[UserModel] clear user cache failed: %v", err.Error()) + } + }() return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { if len(tx) > 0 { conn = tx[0] diff --git a/internal/model/user/cache.go b/internal/model/user/cache.go new file mode 100644 index 0000000..a39f748 --- /dev/null +++ b/internal/model/user/cache.go @@ -0,0 +1,285 @@ +package user + +import ( + "context" + "fmt" + + "github.com/perfect-panel/server/pkg/logger" +) + +type CacheKeyGenerator interface { + GetCacheKeys() []string +} + +type CacheManager interface { + ClearCache(ctx context.Context, keys ...string) error + ClearModelCache(ctx context.Context, models ...CacheKeyGenerator) error +} + +type UserCacheManager struct { + model *defaultUserModel +} + +func NewUserCacheManager(model *defaultUserModel) *UserCacheManager { + return &UserCacheManager{ + model: model, + } +} + +func (c *UserCacheManager) ClearCache(ctx context.Context, keys ...string) error { + if len(keys) == 0 { + return nil + } + return c.model.CachedConn.DelCacheCtx(ctx, keys...) +} + +func (c *UserCacheManager) ClearModelCache(ctx context.Context, models ...CacheKeyGenerator) error { + var allKeys []string + for _, model := range models { + if model != nil { + allKeys = append(allKeys, model.GetCacheKeys()...) + } + } + return c.ClearCache(ctx, allKeys...) +} + +func (u *User) GetCacheKeys() []string { + if u == nil { + return []string{} + } + keys := []string{ + fmt.Sprintf("%s%d", cacheUserIdPrefix, u.Id), + } + + for _, auth := range u.AuthMethods { + if auth.AuthType == "email" { + keys = append(keys, fmt.Sprintf("%s%s", cacheUserEmailPrefix, auth.AuthIdentifier)) + break + } + } + return keys +} + +func (s *Subscribe) GetCacheKeys() []string { + if s == nil { + return []string{} + } + keys := make([]string, 0) + + if s.Token != "" { + keys = append(keys, fmt.Sprintf("%s%s", cacheUserSubscribeTokenPrefix, s.Token)) + } + if s.UserId != 0 { + keys = append(keys, fmt.Sprintf("%s%d", cacheUserSubscribeUserPrefix, s.UserId)) + } + if s.Id != 0 { + keys = append(keys, fmt.Sprintf("%s%d", cacheUserSubscribeIdPrefix, s.Id)) + } + return keys +} + +func (s *Subscribe) GetExtendedCacheKeys(model *defaultUserModel) []string { + keys := s.GetCacheKeys() + + if s.SubscribeId != 0 && model != nil { + serverKeys := model.getServerRelatedCacheKeys(s.SubscribeId) + keys = append(keys, serverKeys...) + } + + return keys +} + +func (d *Device) GetCacheKeys() []string { + if d == nil { + return []string{} + } + keys := []string{} + + if d.Id != 0 { + keys = append(keys, fmt.Sprintf("%s%d", cacheUserDeviceIdPrefix, d.Id)) + } + if d.Identifier != "" { + keys = append(keys, fmt.Sprintf("%s%s", cacheUserDeviceNumberPrefix, d.Identifier)) + } + return keys +} + +func (a *AuthMethods) GetCacheKeys() []string { + if a == nil { + return []string{} + } + keys := []string{} + + if a.UserId != 0 { + keys = append(keys, fmt.Sprintf("%s%d", cacheUserIdPrefix, a.UserId)) + } + if a.AuthType == "email" && a.AuthIdentifier != "" { + keys = append(keys, fmt.Sprintf("%s%s", cacheUserEmailPrefix, a.AuthIdentifier)) + } + return keys +} + +func (m *defaultUserModel) GetCacheManager() *UserCacheManager { + return NewUserCacheManager(m) +} + +func (m *defaultUserModel) getServerRelatedCacheKeys(subscribeId int64) []string { + // 这里复用了 model.go 中的逻辑,但简化了实现 + keys := []string{} + + if subscribeId == 0 { + return keys + } + + // 这里需要从 getSubscribeCacheKey 方法中提取服务器相关的逻辑 + // 为了避免重复查询,我们可以在需要时才获取 + // 或者可以将这个逻辑移到一个统一的地方 + + return keys +} + +func (m *defaultUserModel) ClearUserCache(ctx context.Context, users ...*User) error { + cacheManager := m.GetCacheManager() + models := make([]CacheKeyGenerator, len(users)) + for i, user := range users { + models[i] = user + } + return cacheManager.ClearModelCache(ctx, models...) +} + +func (m *defaultUserModel) ClearSubscribeCacheByModels(ctx context.Context, subscribes ...*Subscribe) error { + cacheManager := m.GetCacheManager() + models := make([]CacheKeyGenerator, len(subscribes)) + for i, subscribe := range subscribes { + models[i] = subscribe + } + return cacheManager.ClearModelCache(ctx, models...) +} + +func (m *defaultUserModel) ClearDeviceCache(ctx context.Context, devices ...*Device) error { + cacheManager := m.GetCacheManager() + models := make([]CacheKeyGenerator, len(devices)) + for i, device := range devices { + models[i] = device + } + return cacheManager.ClearModelCache(ctx, models...) +} + +func (m *defaultUserModel) ClearAuthMethodCache(ctx context.Context, authMethods ...*AuthMethods) error { + cacheManager := m.GetCacheManager() + models := make([]CacheKeyGenerator, len(authMethods)) + for i, auth := range authMethods { + models[i] = auth + } + return cacheManager.ClearModelCache(ctx, models...) +} + +func (m *defaultUserModel) BatchClearRelatedCache(ctx context.Context, user *User) error { + if user == nil { + return nil + } + + cacheManager := m.GetCacheManager() + + var allModels []CacheKeyGenerator + allModels = append(allModels, user) + + for _, auth := range user.AuthMethods { + allModels = append(allModels, &auth) + } + + for _, device := range user.UserDevices { + allModels = append(allModels, &device) + } + + subscribes, err := m.QueryUserSubscribe(ctx, user.Id) + if err != nil { + logger.Errorf("failed to query user subscribes for cache clearing: %v", err) + } else { + for _, sub := range subscribes { + subModel := &Subscribe{ + Id: sub.Id, + UserId: sub.UserId, + Token: sub.Token, + SubscribeId: sub.SubscribeId, + } + allModels = append(allModels, subModel) + } + } + + return cacheManager.ClearModelCache(ctx, allModels...) +} + +func (m *defaultUserModel) CacheInvalidationHandler(ctx context.Context, operation string, modelType string, model interface{}) error { + switch operation { + case "create", "update", "delete": + switch modelType { + case "user": + if user, ok := model.(*User); ok { + return m.BatchClearRelatedCache(ctx, user) + } + case "subscribe": + if subscribe, ok := model.(*Subscribe); ok { + return m.ClearSubscribeCacheByModels(ctx, subscribe) + } + case "device": + if device, ok := model.(*Device); ok { + return m.ClearDeviceCache(ctx, device) + } + case "authmethod": + if authMethod, ok := model.(*AuthMethods); ok { + return m.ClearAuthMethodCache(ctx, authMethod) + } + } + } + return nil +} + +func (m *customUserModel) GetRelatedCacheKeys(ctx context.Context, modelType string, modelId int64) ([]string, error) { + var keys []string + + switch modelType { + case "user": + user, err := m.FindOne(ctx, modelId) + if err != nil { + return nil, err + } + keys = append(keys, user.GetCacheKeys()...) + + auths, err := m.FindUserAuthMethods(ctx, modelId) + if err == nil { + for _, auth := range auths { + keys = append(keys, auth.GetCacheKeys()...) + } + } + + subscribes, err := m.QueryUserSubscribe(ctx, modelId) + if err == nil { + for _, sub := range subscribes { + subModel := &Subscribe{ + Id: sub.Id, + UserId: sub.UserId, + Token: sub.Token, + SubscribeId: sub.SubscribeId, + } + keys = append(keys, subModel.GetCacheKeys()...) + } + } + + case "subscribe": + subscribe, err := m.FindOneSubscribe(ctx, modelId) + if err != nil { + return nil, err + } + keys = append(keys, subscribe.GetCacheKeys()...) + + case "device": + device, err := m.FindOneDevice(ctx, modelId) + if err != nil { + return nil, err + } + keys = append(keys, device.GetCacheKeys()...) + } + + return keys, nil +} diff --git a/internal/model/user/default.go b/internal/model/user/default.go index 94a47ed..e2a326a 100644 --- a/internal/model/user/default.go +++ b/internal/model/user/default.go @@ -48,29 +48,20 @@ func newUserModel(db *gorm.DB, c *redis.Client) *defaultUserModel { func (m *defaultUserModel) batchGetCacheKeys(users ...*User) []string { var keys []string for _, user := range users { - keys = append(keys, m.getCacheKeys(user)...) + keys = append(keys, user.GetCacheKeys()...) } return keys - } + func (m *defaultUserModel) getCacheKeys(data *User) []string { if data == nil { return []string{} } - userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, data.Id) - cacheKeys := []string{ - userIdKey, - } - // email key - if len(data.AuthMethods) > 0 { - for _, auth := range data.AuthMethods { - if auth.AuthType == "email" { - cacheKeys = append(cacheKeys, fmt.Sprintf("%s%v", cacheUserEmailPrefix, auth.AuthIdentifier)) - break - } - } - } - return cacheKeys + return data.GetCacheKeys() +} + +func (m *defaultUserModel) clearUserCache(ctx context.Context, data ...*User) error { + return m.ClearUserCache(ctx, data...) } func (m *defaultUserModel) FindOneByEmail(ctx context.Context, email string) (*User, error) { @@ -127,53 +118,38 @@ func (m *defaultUserModel) Delete(ctx context.Context, id int64, tx ...*gorm.DB) } return err } - err = m.ExecCtx(ctx, func(conn *gorm.DB) error { - if len(tx) > 0 { - conn = tx[0] + + // 使用批量相关缓存清理,包含所有相关数据的缓存 + defer func() { + if clearErr := m.BatchClearRelatedCache(ctx, data); clearErr != nil { + // 记录清理缓存错误,但不阻断删除操作 } - return conn.Transaction(func(db *gorm.DB) error { - if err := db.Model(&User{}).Where("`id` = ?", id).Delete(&User{}).Error; err != nil { - return err - } - if err := db.Model(&AuthMethods{}).Where("`user_id` = ?", id).Delete(&User{}).Error; err != nil { - return err - } - if err := db.Model(&Subscribe{}).Where("`user_id` = ?", id).Delete(&User{}).Error; err != nil { - return err - } - if err := db.Model(&BalanceLog{}).Where("`user_id` = ?", id).Delete(&User{}).Error; err != nil { - return err - } - if err := db.Model(&GiftAmountLog{}).Where("`user_id` = ?", id).Delete(&User{}).Error; err != nil { - return err - } - if err := db.Model(&LoginLog{}).Where("`user_id` = ?", id).Delete(&User{}).Error; err != nil { - return err - } - if err := db.Model(&SubscribeLog{}).Where("`user_id` = ?", id).Delete(&User{}).Error; err != nil { - return err - } - if err := db.Model(&Device{}).Where("`user_id` = ?", id).Delete(&User{}).Error; err != nil { - return err - } + }() - subs, err := m.QueryUserSubscribe(ctx, id) - if err != nil { - return err - } - for _, sub := range subs { - if err := m.DeleteSubscribeById(ctx, sub.Id, db); err != nil { - return err - } - } + return m.TransactCtx(ctx, func(db *gorm.DB) error { + if len(tx) > 0 { + db = tx[0] + } - if err := db.Model(&CommissionLog{}).Where("`user_id` = ?", id).Delete(&User{}).Error; err != nil { - return err - } - return nil - }) - }, m.getCacheKeys(data)...) - return err + // 删除用户相关的所有数据 + if err := db.Model(&User{}).Where("`id` = ?", id).Delete(&User{}).Error; err != nil { + return err + } + + if err := db.Model(&AuthMethods{}).Where("`user_id` = ?", id).Delete(&AuthMethods{}).Error; err != nil { + return err + } + + if err := db.Model(&Subscribe{}).Where("`user_id` = ?", id).Delete(&Subscribe{}).Error; err != nil { + return err + } + + if err := db.Model(&Device{}).Where("`user_id` = ?", id).Delete(&Device{}).Error; err != nil { + return err + } + + return nil + }) } func (m *defaultUserModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error { diff --git a/internal/model/user/device.go b/internal/model/user/device.go index 7258819..b1194a7 100644 --- a/internal/model/user/device.go +++ b/internal/model/user/device.go @@ -46,18 +46,27 @@ func (m *customUserModel) QueryDevicePageList(ctx context.Context, userId, subsc return list, total, err } +// QueryDeviceList returns a list of records that meet the conditions. +func (m *customUserModel) QueryDeviceList(ctx context.Context, userId int64) ([]*Device, int64, error) { + var list []*Device + var total int64 + err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Device{}).Where("`user_id` = ?", userId).Count(&total).Find(&list).Error + }) + return list, total, err +} + func (m *customUserModel) UpdateDevice(ctx context.Context, data *Device, tx ...*gorm.DB) error { old, err := m.FindOneDevice(ctx, data.Id) if err != nil { return err } - deviceIdKey := fmt.Sprintf("%s%v", cacheUserDeviceIdPrefix, old.Id) err = m.ExecCtx(ctx, func(conn *gorm.DB) error { if len(tx) > 0 { conn = tx[0] } return conn.Save(data).Error - }, deviceIdKey) + }, old.GetCacheKeys()...) return err } @@ -69,12 +78,26 @@ func (m *customUserModel) DeleteDevice(ctx context.Context, id int64, tx ...*gor } return err } - deviceIdKey := fmt.Sprintf("%s%v", cacheUserDeviceIdPrefix, data.Id) err = m.ExecCtx(ctx, func(conn *gorm.DB) error { if len(tx) > 0 { conn = tx[0] } return conn.Delete(&Device{}, id).Error - }, deviceIdKey) + }, data.GetCacheKeys()...) return err } + +func (m *customUserModel) InsertDevice(ctx context.Context, data *Device, tx ...*gorm.DB) error { + defer func() { + if clearErr := m.ClearDeviceCache(ctx, data); clearErr != nil { + // log cache clear error + } + }() + + return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { + if len(tx) > 0 { + conn = tx[0] + } + return conn.Create(data).Error + }) +} diff --git a/internal/model/user/log.go b/internal/model/user/log.go deleted file mode 100644 index d3e1105..0000000 --- a/internal/model/user/log.go +++ /dev/null @@ -1,81 +0,0 @@ -package user - -import ( - "context" - - "github.com/pkg/errors" - "gorm.io/gorm" -) - -func (m *customUserModel) InsertSubscribeLog(ctx context.Context, log *SubscribeLog) error { - return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { - return conn.Create(log).Error - }) -} - -func (m *customUserModel) FilterSubscribeLogList(ctx context.Context, page, size int, filter *SubscribeLogFilterParams) ([]*SubscribeLog, int64, error) { - var list []*SubscribeLog - var total int64 - err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { - query := conn.Model(&SubscribeLog{}) - if filter != nil { - if filter.UserId != 0 { - query = query.Where("user_id = ?", filter.UserId) - } - if filter.UserSubscribeId != 0 { - query = query.Where("user_subscribe_id = ?", filter.UserSubscribeId) - } - if filter.IP != "" { - query = query.Where("ip LIKE ?", "%"+filter.IP+"%") - } - if filter.Token != "" { - query = query.Where("token LIKE ?", "%"+filter.Token+"%") - } - if filter.UserAgent != "" { - query = query.Where("user_agent LIKE ?", "%"+filter.UserAgent+"%") - } - } - return query.Count(&total).Limit(size).Offset((page - 1) * size).Find(v).Error - }) - - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return nil, 0, err - } - - return list, total, nil -} - -func (m *customUserModel) InsertLoginLog(ctx context.Context, log *LoginLog) error { - return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { - return conn.Create(log).Error - }) -} - -func (m *customUserModel) FilterLoginLogList(ctx context.Context, page, size int, filter *LoginLogFilterParams) ([]*LoginLog, int64, error) { - var list []*LoginLog - var total int64 - err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { - query := conn.Model(&LoginLog{}) - if filter != nil { - if filter.UserId != 0 { - query = query.Where("user_id = ?", filter.UserId) - } - if filter.IP != "" { - query = query.Where("ip LIKE ?", "%"+filter.IP+"%") - } - if filter.UserAgent != "" { - query = query.Where("user_agent LIKE ?", "%"+filter.UserAgent+"%") - } - if filter.Success != nil { - query = query.Where("success = ?", *filter.Success) - } - } - return query.Count(&total).Limit(size).Offset((page - 1) * size).Find(v).Error - }) - - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return nil, 0, err - } - - return list, total, nil -} diff --git a/internal/model/user/model.go b/internal/model/user/model.go index 7240c3c..86caa0d 100644 --- a/internal/model/user/model.go +++ b/internal/model/user/model.go @@ -2,15 +2,12 @@ package user import ( "context" - "errors" "fmt" "time" - "github.com/perfect-panel/server/internal/config" - "github.com/perfect-panel/server/internal/model/server" + "github.com/perfect-panel/server/internal/model/order" "github.com/perfect-panel/server/internal/model/subscribe" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/tool" + "github.com/redis/go-redis/v9" "gorm.io/gorm" ) @@ -39,6 +36,7 @@ type SubscribeDetails struct { Token string `gorm:"index:idx_token;unique;type:varchar(255);default:'';comment:Token"` UUID string `gorm:"type:varchar(255);unique;index:idx_uuid;default:'';comment:UUID"` Status uint8 `gorm:"type:tinyint(1);default:0;comment:Subscription Status: 0: Pending 1: Active 2: Finished 3: Expired; 4: Cancelled"` + Note string `gorm:"type:varchar(500);default:'';comment:User note for subscription"` CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` UpdatedAt time.Time `gorm:"comment:Update Time"` } @@ -63,6 +61,7 @@ type UserFilterParams struct { UserId *int64 SubscribeId *int64 UserSubscribeId *int64 + Order string // Order by id, e.g., "desc" } type customUserLogicModel interface { @@ -79,7 +78,6 @@ type customUserLogicModel interface { QueryUserSubscribe(ctx context.Context, userId int64, status ...int64) ([]*SubscribeDetails, error) FindOneSubscribeDetailsById(ctx context.Context, id int64) (*SubscribeDetails, error) FindOneUserSubscribe(ctx context.Context, id int64) (*SubscribeDetails, error) - InsertBalanceLog(ctx context.Context, data *BalanceLog, tx ...*gorm.DB) error FindUsersSubscribeBySubscribeId(ctx context.Context, subscribeId int64) ([]*Subscribe, error) UpdateUserSubscribeWithTraffic(ctx context.Context, id, download, upload int64, tx ...*gorm.DB) error QueryResisterUserTotalByDate(ctx context.Context, date time.Time) (int64, error) @@ -88,7 +86,6 @@ type customUserLogicModel interface { QueryAdminUsers(ctx context.Context) ([]*User, error) UpdateUserCache(ctx context.Context, data *User) error UpdateUserSubscribeCache(ctx context.Context, data *Subscribe) error - InsertCommissionLog(ctx context.Context, data *CommissionLog, tx ...*gorm.DB) error QueryActiveSubscriptions(ctx context.Context, subscribeId ...int64) (map[int64]int64, error) FindUserAuthMethods(ctx context.Context, userId int64) ([]*AuthMethods, error) InsertUserAuthMethods(ctx context.Context, data *AuthMethods, tx ...*gorm.DB) error @@ -99,23 +96,25 @@ type customUserLogicModel interface { FindUserAuthMethodByPlatform(ctx context.Context, userId int64, platform string) (*AuthMethods, error) FindOneByEmail(ctx context.Context, email string) (*User, error) FindOneDevice(ctx context.Context, id int64) (*Device, error) + QueryDeviceList(ctx context.Context, userid int64) ([]*Device, int64, error) QueryDevicePageList(ctx context.Context, userid, subscribeId int64, page, size int) ([]*Device, int64, error) UpdateDevice(ctx context.Context, data *Device, tx ...*gorm.DB) error FindOneDeviceByIdentifier(ctx context.Context, id string) (*Device, error) DeleteDevice(ctx context.Context, id int64, tx ...*gorm.DB) error - - InsertSubscribeLog(ctx context.Context, log *SubscribeLog) error - FilterSubscribeLogList(ctx context.Context, page, size int, filter *SubscribeLogFilterParams) ([]*SubscribeLog, int64, error) - InsertLoginLog(ctx context.Context, log *LoginLog) error - FilterLoginLogList(ctx context.Context, page, size int, filter *LoginLogFilterParams) ([]*LoginLog, int64, error) + InsertDevice(ctx context.Context, data *Device, tx ...*gorm.DB) error ClearSubscribeCache(ctx context.Context, data ...*Subscribe) error + ClearUserCache(ctx context.Context, data ...*User) error - InsertResetSubscribeLog(ctx context.Context, log *ResetSubscribeLog, tx ...*gorm.DB) error - UpdateResetSubscribeLog(ctx context.Context, log *ResetSubscribeLog, tx ...*gorm.DB) error - FindResetSubscribeLog(ctx context.Context, id int64) (*ResetSubscribeLog, error) - DeleteResetSubscribeLog(ctx context.Context, id int64, tx ...*gorm.DB) error - FilterResetSubscribeLogList(ctx context.Context, filter *FilterResetSubscribeLogParams) ([]*ResetSubscribeLog, int64, error) + QueryDailyUserStatisticsList(ctx context.Context, date time.Time) ([]UserStatisticsWithDate, error) + QueryMonthlyUserStatisticsList(ctx context.Context, date time.Time) ([]UserStatisticsWithDate, error) +} + +type UserStatisticsWithDate struct { + Date string + Register int64 + NewOrderUsers int64 + RenewalOrderUsers int64 } // NewModel returns a model for the database table. @@ -125,56 +124,6 @@ func NewModel(conn *gorm.DB, c *redis.Client) Model { } } -func (m *defaultUserModel) getSubscribeCacheKey(data *Subscribe) []string { - if data == nil { - return []string{} - } - var keys []string - if data.Token != "" { - keys = append(keys, fmt.Sprintf("%s%s", cacheUserSubscribeTokenPrefix, data.Token)) - } - if data.UserId != 0 { - keys = append(keys, fmt.Sprintf("%s%d", cacheUserSubscribeUserPrefix, data.UserId)) - } - if data.Id != 0 { - keys = append(keys, fmt.Sprintf("%s%d", cacheUserSubscribeIdPrefix, data.Id)) - } - - if data.SubscribeId != 0 { - var sub *subscribe.Subscribe - err := m.QueryNoCacheCtx(context.Background(), &sub, func(conn *gorm.DB, v interface{}) error { - return conn.Model(&subscribe.Subscribe{}).Where("id = ?", data.SubscribeId).First(&sub).Error - }) - if err != nil { - logger.Error("getUserSubscribeCacheKey", logger.Field("error", err.Error()), logger.Field("subscribeId", data.SubscribeId)) - return keys - } - if sub.Server != "" { - ids := tool.StringToInt64Slice(sub.Server) - for _, id := range ids { - keys = append(keys, fmt.Sprintf("%s%d", config.ServerUserListCacheKey, id)) - } - } - if sub.ServerGroup != "" { - ids := tool.StringToInt64Slice(sub.ServerGroup) - var servers []*server.Server - err = m.QueryNoCacheCtx(context.Background(), &servers, func(conn *gorm.DB, v interface{}) error { - return conn.Model(&server.Server{}).Where("group_id in ?", ids).Find(v).Error - }) - if err != nil { - logger.Error("getUserSubscribeCacheKey", logger.Field("error", err.Error()), logger.Field("subscribeId", data.SubscribeId)) - return keys - } - for _, s := range servers { - keys = append(keys, fmt.Sprintf("%s%d", config.ServerUserListCacheKey, s.Id)) - } - } - } - - return keys - -} - // QueryPageList returns a list of records that meet the conditions. func (m *customUserModel) QueryPageList(ctx context.Context, page, size int, filter *UserFilterParams) ([]*User, int64, error) { var list []*User @@ -196,6 +145,9 @@ func (m *customUserModel) QueryPageList(ctx context.Context, page, size int, fil conn = conn.Joins("LEFT JOIN user_subscribe ON user.id = user_subscribe.user_id"). Where("user_subscribe.subscribe_id =? and `status` IN (0,1)", *filter.SubscribeId) } + if filter.Order != "" { + conn = conn.Order(fmt.Sprintf("user.id %s", filter.Order)) + } } return conn.Model(&User{}).Group("user.id").Count(&total).Limit(size).Offset((page - 1) * size).Preload("UserDevices").Preload("AuthMethods").Find(&list).Error }) @@ -219,33 +171,20 @@ func (m *customUserModel) BatchDeleteUser(ctx context.Context, ids []int64, tx . }, m.batchGetCacheKeys(users...)...) } -// InsertBalanceLog insert BalanceLog into the database. -func (m *customUserModel) InsertBalanceLog(ctx context.Context, data *BalanceLog, tx ...*gorm.DB) error { - return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { - if len(tx) > 0 { - conn = tx[0] - } - return conn.Create(data).Error - }) -} - -// FindUserBalanceLogList returns a list of records that meet the conditions. -func (m *customUserModel) FindUserBalanceLogList(ctx context.Context, userId int64, page, size int) ([]*BalanceLog, int64, error) { - var list []*BalanceLog - var total int64 - err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { - - return conn.Model(&BalanceLog{}).Where("`user_id` = ?", userId).Count(&total).Limit(size).Offset((page - 1) * size).Find(&list).Error - }) - return list, total, err -} - func (m *customUserModel) UpdateUserSubscribeWithTraffic(ctx context.Context, id, download, upload int64, tx ...*gorm.DB) error { sub, err := m.FindOneSubscribe(ctx, id) if err != nil { return err } - return m.ExecCtx(ctx, func(conn *gorm.DB) error { + + // 使用 defer 确保更新后清理缓存 + defer func() { + if clearErr := m.ClearSubscribeCacheByModels(ctx, sub); clearErr != nil { + // 记录清理缓存错误 + } + }() + + return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { if len(tx) > 0 { conn = tx[0] } @@ -253,7 +192,7 @@ func (m *customUserModel) UpdateUserSubscribeWithTraffic(ctx context.Context, id "download": gorm.Expr("download + ?", download), "upload": gorm.Expr("upload + ?", upload), }).Error - }, m.getSubscribeCacheKey(sub)...) + }) } func (m *customUserModel) QueryResisterUserTotalByDate(ctx context.Context, date time.Time) (int64, error) { @@ -293,16 +232,7 @@ func (m *customUserModel) QueryAdminUsers(ctx context.Context) ([]*User, error) } func (m *customUserModel) UpdateUserCache(ctx context.Context, data *User) error { - return m.CachedConn.DelCacheCtx(ctx, m.getCacheKeys(data)...) -} - -func (m *customUserModel) InsertCommissionLog(ctx context.Context, data *CommissionLog, tx ...*gorm.DB) error { - return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { - if len(tx) > 0 { - conn = tx[0] - } - return conn.Model(&CommissionLog{}).Create(data).Error - }) + return m.ClearUserCache(ctx, data) } func (m *customUserModel) FindOneByReferCode(ctx context.Context, referCode string) (*User, error) { @@ -321,81 +251,77 @@ func (m *customUserModel) FindOneSubscribeDetailsById(ctx context.Context, id in return &data, err } -func (m *customUserModel) InsertResetSubscribeLog(ctx context.Context, log *ResetSubscribeLog, tx ...*gorm.DB) error { - return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { - if len(tx) > 0 { - conn = tx[0] - } - return conn.Model(&ResetSubscribeLog{}).Create(log).Error - }) -} +// QueryDailyUserStatisticsList Query daily user statistics list for the current month (from 1st to current date) +func (m *customUserModel) QueryDailyUserStatisticsList(ctx context.Context, date time.Time) ([]UserStatisticsWithDate, error) { + var results []UserStatisticsWithDate -func (m *customUserModel) UpdateResetSubscribeLog(ctx context.Context, log *ResetSubscribeLog, tx ...*gorm.DB) error { - return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { - if len(tx) > 0 { - conn = tx[0] - } - return conn.Model(&ResetSubscribeLog{}).Where("id = ?", log.Id).Updates(log).Error - }) -} + err := m.QueryNoCacheCtx(ctx, &results, func(conn *gorm.DB, v interface{}) error { + firstDay := time.Date(date.Year(), date.Month(), 1, 0, 0, 0, 0, date.Location()) -func (m *customUserModel) FindResetSubscribeLog(ctx context.Context, id int64) (*ResetSubscribeLog, error) { - var data ResetSubscribeLog - err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error { - return conn.Model(&ResetSubscribeLog{}).Where("id = ?", id).First(&data).Error - }) - return &data, err -} + // 子查询:统计每天的新用户订单数量 + newOrderSub := conn.Model(&order.Order{}). + Select("DATE_FORMAT(created_at, '%Y-%m-%d') AS date, COUNT(DISTINCT user_id) AS new_order_users"). + Where("is_new = 1 AND created_at BETWEEN ? AND ? AND status IN ?", firstDay, date, []int64{2, 5}). + Group("DATE_FORMAT(created_at, '%Y-%m-%d')") -func (m *customUserModel) DeleteResetSubscribeLog(ctx context.Context, id int64, tx ...*gorm.DB) error { - return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { - if len(tx) > 0 { - conn = tx[0] - } - return conn.Model(&ResetSubscribeLog{}).Where("id = ?", id).Delete(&ResetSubscribeLog{}).Error - }) -} + // 子查询:统计每天的续费订单数量 + renewalOrderSub := conn.Model(&order.Order{}). + Select("DATE_FORMAT(created_at, '%Y-%m-%d') AS date, COUNT(DISTINCT user_id) AS renewal_order_users"). + Where("is_new = 0 AND created_at BETWEEN ? AND ? AND status IN ?", firstDay, date, []int64{2, 5}). + Group("DATE_FORMAT(created_at, '%Y-%m-%d')") -func (m *customUserModel) FilterResetSubscribeLogList(ctx context.Context, filter *FilterResetSubscribeLogParams) ([]*ResetSubscribeLog, int64, error) { - if filter == nil { - return nil, 0, errors.New("filter params is nil") - } - - var list []*ResetSubscribeLog - var total int64 - - err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { - query := conn.Model(&ResetSubscribeLog{}) - - // 应用筛选条件 - if filter.UserId != 0 { - query = query.Where("user_id = ?", filter.UserId) - } - if filter.UserSubscribeId != 0 { - query = query.Where("user_subscribe_id = ?", filter.UserSubscribeId) - } - if filter.Type != 0 { - query = query.Where("type = ?", filter.Type) - } - if filter.OrderNo != "" { - query = query.Where("order_no = ?", filter.OrderNo) - } - - // 计算总数 - if err := query.Count(&total).Error; err != nil { - return err - } - - // 应用分页 - if filter.Page > 0 && filter.Size > 0 { - query = query.Offset((filter.Page - 1) * filter.Size) - } - if filter.Size > 0 { - query = query.Limit(filter.Size) - } - - return query.Find(&list).Error + return conn.Model(&User{}). + Select(` + DATE_FORMAT(user.created_at, '%Y-%m-%d') AS date, + COUNT(*) AS register, + IFNULL(MAX(n.new_order_users), 0) AS new_order_users, + IFNULL(MAX(r.renewal_order_users), 0) AS renewal_order_users + `). + Joins("LEFT JOIN (?) AS n ON DATE_FORMAT(user.created_at, '%Y-%m-%d') = n.date", newOrderSub). + Joins("LEFT JOIN (?) AS r ON DATE_FORMAT(user.created_at, '%Y-%m-%d') = r.date", renewalOrderSub). + Where("user.created_at BETWEEN ? AND ?", firstDay, date). + Group("DATE_FORMAT(user.created_at, '%Y-%m-%d')"). + Order("date ASC"). + Scan(v).Error }) - return list, total, err + return results, err +} + +// QueryMonthlyUserStatisticsList Query monthly user statistics list for the past 6 months +func (m *customUserModel) QueryMonthlyUserStatisticsList(ctx context.Context, date time.Time) ([]UserStatisticsWithDate, error) { + var results []UserStatisticsWithDate + + err := m.QueryNoCacheCtx(ctx, &results, func(conn *gorm.DB, v interface{}) error { + // 获取 6 个月前的日期 + sixMonthsAgo := date.AddDate(0, -5, 0) + + // 子查询:每月新订单用户数量 + newOrderSub := conn.Model(&order.Order{}). + Select("DATE_FORMAT(created_at, '%Y-%m') AS date, COUNT(DISTINCT user_id) AS new_order_users"). + Where("is_new = 1 AND created_at >= ? AND status IN ?", sixMonthsAgo, []int64{2, 5}). + Group("DATE_FORMAT(created_at, '%Y-%m')") + + // 子查询:每月续费订单用户数量 + renewalOrderSub := conn.Model(&order.Order{}). + Select("DATE_FORMAT(created_at, '%Y-%m') AS date, COUNT(DISTINCT user_id) AS renewal_order_users"). + Where("is_new = 0 AND created_at >= ? AND status IN ?", sixMonthsAgo, []int64{2, 5}). + Group("DATE_FORMAT(created_at, '%Y-%m')") + + return conn.Model(&User{}). + Select(` + DATE_FORMAT(user.created_at, '%Y-%m') AS date, + COUNT(*) AS register, + IFNULL(MAX(n.new_order_users), 0) AS new_order_users, + IFNULL(MAX(r.renewal_order_users), 0) AS renewal_order_users + `). + Joins("LEFT JOIN (?) AS n ON DATE_FORMAT(user.created_at, '%Y-%m') = n.date", newOrderSub). + Joins("LEFT JOIN (?) AS r ON DATE_FORMAT(user.created_at, '%Y-%m') = r.date", renewalOrderSub). + Where("user.created_at >= ?", sixMonthsAgo). + Group("DATE_FORMAT(user.created_at, '%Y-%m')"). + Order("date ASC"). + Scan(v).Error + }) + + return results, err } diff --git a/internal/model/user/subscribe.go b/internal/model/user/subscribe.go index e0f5ca6..d8d81fb 100644 --- a/internal/model/user/subscribe.go +++ b/internal/model/user/subscribe.go @@ -9,7 +9,7 @@ import ( ) func (m *defaultUserModel) UpdateUserSubscribeCache(ctx context.Context, data *Subscribe) error { - return m.CachedConn.DelCacheCtx(ctx, m.getSubscribeCacheKey(data)...) + return m.ClearSubscribeCacheByModels(ctx, data) } // QueryActiveSubscriptions returns the number of active subscriptions. @@ -21,7 +21,7 @@ func (m *defaultUserModel) QueryActiveSubscriptions(ctx context.Context, subscri var result []SubscriptionCount err := m.QueryNoCacheCtx(ctx, &result, func(conn *gorm.DB, v interface{}) error { return conn.Model(&Subscribe{}). - Where("subscribe_id IN ? AND `status` IN ?", subscribeId, []int64{1, 0, 3}). + Where("subscribe_id IN ? AND `status` IN ?", subscribeId, []int64{1, 0}). Select("subscribe_id, COUNT(id) as total"). Group("subscribe_id"). Scan(&result). @@ -60,7 +60,13 @@ func (m *defaultUserModel) FindOneSubscribe(ctx context.Context, id int64) (*Sub func (m *defaultUserModel) FindUsersSubscribeBySubscribeId(ctx context.Context, subscribeId int64) ([]*Subscribe, error) { var data []*Subscribe err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error { - return conn.Model(&Subscribe{}).Where("subscribe_id = ? AND `status` IN ?", subscribeId, []int64{1, 0}).Find(&data).Error + err := conn.Model(&Subscribe{}).Where("subscribe_id = ? AND `status` IN ?", subscribeId, []int64{1, 0}).Find(v).Error + + if err != nil { + return err + } + // update user subscribe status + return conn.Model(&Subscribe{}).Where("subscribe_id = ? AND `status` = ?", subscribeId, 0).Update("status", 1).Error }) return data, err } @@ -114,12 +120,20 @@ func (m *defaultUserModel) UpdateSubscribe(ctx context.Context, data *Subscribe, if err != nil { return err } - return m.ExecCtx(ctx, func(conn *gorm.DB) error { + + // 使用 defer 确保更新后清理缓存 + defer func() { + if clearErr := m.ClearSubscribeCacheByModels(ctx, old, data); clearErr != nil { + // 记录清理缓存错误 + } + }() + + return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { if len(tx) > 0 { conn = tx[0] } return conn.Model(&Subscribe{}).Where("id = ?", data.Id).Save(data).Error - }, m.getSubscribeCacheKey(old)...) + }) } // DeleteSubscribe deletes a record. @@ -128,22 +142,37 @@ func (m *defaultUserModel) DeleteSubscribe(ctx context.Context, token string, tx if err != nil { return err } - return m.ExecCtx(ctx, func(conn *gorm.DB) error { + + // 使用 defer 确保删除后清理缓存 + defer func() { + if clearErr := m.ClearSubscribeCacheByModels(ctx, data); clearErr != nil { + // 记录清理缓存错误 + } + }() + + return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { if len(tx) > 0 { conn = tx[0] } return conn.Where("token = ?", token).Delete(&Subscribe{}).Error - }, m.getSubscribeCacheKey(data)...) + }) } // InsertSubscribe insert Subscribe into the database. func (m *defaultUserModel) InsertSubscribe(ctx context.Context, data *Subscribe, tx ...*gorm.DB) error { - return m.ExecCtx(ctx, func(conn *gorm.DB) error { + // 使用 defer 确保插入后清理相关缓存 + defer func() { + if clearErr := m.ClearSubscribeCacheByModels(ctx, data); clearErr != nil { + // 记录清理缓存错误 + } + }() + + return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { if len(tx) > 0 { conn = tx[0] } return conn.Create(data).Error - }, m.getSubscribeCacheKey(data)...) + }) } func (m *defaultUserModel) DeleteSubscribeById(ctx context.Context, id int64, tx ...*gorm.DB) error { @@ -151,18 +180,22 @@ func (m *defaultUserModel) DeleteSubscribeById(ctx context.Context, id int64, tx if err != nil { return err } - return m.ExecCtx(ctx, func(conn *gorm.DB) error { + + // 使用 defer 确保删除后清理缓存 + defer func() { + if clearErr := m.ClearSubscribeCacheByModels(ctx, data); clearErr != nil { + // 记录清理缓存错误 + } + }() + + return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { if len(tx) > 0 { conn = tx[0] } return conn.Where("id = ?", id).Delete(&Subscribe{}).Error - }, m.getSubscribeCacheKey(data)...) + }) } func (m *defaultUserModel) ClearSubscribeCache(ctx context.Context, data ...*Subscribe) error { - var keys []string - for _, item := range data { - keys = append(keys, m.getSubscribeCacheKey(item)...) - } - return m.CachedConn.DelCacheCtx(ctx, keys...) + return m.ClearSubscribeCacheByModels(ctx, data...) } diff --git a/internal/model/user/user.go b/internal/model/user/user.go index 9c3685f..923594e 100644 --- a/internal/model/user/user.go +++ b/internal/model/user/user.go @@ -2,19 +2,20 @@ package user import ( "time" - - "gorm.io/gorm" - "gorm.io/plugin/soft_delete" ) type User struct { Id int64 `gorm:"primaryKey"` Password string `gorm:"type:varchar(100);not null;comment:User Password"` + Algo string `gorm:"type:varchar(20);default:'default';comment:Encryption Algorithm"` + Salt string `gorm:"type:varchar(20);default:null;comment:Password Salt"` Avatar string `gorm:"type:MEDIUMTEXT;comment:User Avatar"` Balance int64 `gorm:"default:0;comment:User Balance"` // User Balance Amount ReferCode string `gorm:"type:varchar(20);default:'';comment:Referral Code"` RefererId int64 `gorm:"index:idx_referer;comment:Referrer ID"` - Commission int64 `gorm:"default:0;comment:Commission"` // Commission Amount + Commission int64 `gorm:"default:0;comment:Commission"` // Commission Amount + ReferralPercentage uint8 `gorm:"default:0;comment:Referral"` // Referral Percentage + OnlyFirstPurchase *bool `gorm:"default:true;not null;comment:Only First Purchase"` // Only First Purchase Referral GiftAmount int64 `gorm:"default:0;comment:User Gift Amount"` Enable *bool `gorm:"default:true;not null;comment:Is Account Enabled"` IsAdmin *bool `gorm:"default:false;not null;comment:Is Admin"` @@ -24,43 +25,12 @@ type User struct { EnableTradeNotify *bool `gorm:"default:false;not null;comment:Enable Trade Notifications"` AuthMethods []AuthMethods `gorm:"foreignKey:UserId;references:Id"` UserDevices []Device `gorm:"foreignKey:UserId;references:Id"` + Rules string `gorm:"type:TEXT;comment:User Rules"` CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` UpdatedAt time.Time `gorm:"comment:Update Time"` } -func (User) TableName() string { - return "user" -} - -type OldUser struct { - Id int64 `gorm:"primaryKey"` - Email string `gorm:"index:idx_email;type:varchar(100);comment:Email"` - //Telephone string `gorm:"index:idx_telephone;type:varchar(20);default:'';comment:Telephone"` - //TelephoneAreaCode string `gorm:"index:idx_telephone;type:varchar(20);default:'';comment:TelephoneAreaCode"` - Password string `gorm:"type:varchar(100);not null;comment:User Password"` - Avatar string `gorm:"type:varchar(200);default:'';comment:User Avatar"` - Balance int64 `gorm:"default:0;comment:User Balance"` // User Balance Amount - Telegram int64 `gorm:"default:null;comment:Telegram Account"` - ReferCode string `gorm:"type:varchar(20);default:'';comment:Referral Code"` - RefererId int64 `gorm:"index:idx_referer;comment:Referrer ID"` - Commission int64 `gorm:"default:0;comment:Commission"` // Commission Amount - GiftAmount int64 `gorm:"default:0;comment:User Gift Amount"` - Enable *bool `gorm:"default:true;not null;comment:Is Account Enabled"` - IsAdmin *bool `gorm:"default:false;not null;comment:Is Admin"` - ValidEmail *bool `gorm:"default:false;not null;comment:Is Email Verified"` - EnableEmailNotify *bool `gorm:"default:false;not null;comment:Enable Email Notifications"` - EnableTelegramNotify *bool `gorm:"default:false;not null;comment:Enable Telegram Notifications"` - EnableBalanceNotify *bool `gorm:"default:false;not null;comment:Enable Balance Change Notifications"` - EnableLoginNotify *bool `gorm:"default:false;not null;comment:Enable Login Notifications"` - EnableSubscribeNotify *bool `gorm:"default:false;not null;comment:Enable Subscription Notifications"` - EnableTradeNotify *bool `gorm:"default:false;not null;comment:Enable Trade Notifications"` - CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` - UpdatedAt time.Time `gorm:"comment:Update Time"` - DeletedAt gorm.DeletedAt `gorm:"default:null;comment:Deletion Time"` - IsDel soft_delete.DeletedAt `gorm:"softDelete:flag,DeletedAtField:DeletedAt;comment:1: Normal 0: Deleted"` // Using `1` and `0` to indicate -} - -func (OldUser) TableName() string { +func (*User) TableName() string { return "user" } @@ -79,56 +49,15 @@ type Subscribe struct { Token string `gorm:"index:idx_token;unique;type:varchar(255);default:'';comment:Token"` UUID string `gorm:"type:varchar(255);unique;index:idx_uuid;default:'';comment:UUID"` Status uint8 `gorm:"type:tinyint(1);default:0;comment:Subscription Status: 0: Pending 1: Active 2: Finished 3: Expired 4: Deducted"` + Note string `gorm:"type:varchar(500);default:'';comment:User note for subscription"` CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` UpdatedAt time.Time `gorm:"comment:Update Time"` } -func (Subscribe) TableName() string { +func (*Subscribe) TableName() string { return "user_subscribe" } -type BalanceLog struct { - Id int64 `gorm:"primaryKey"` - UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"` - Amount int64 `gorm:"not null;comment:Amount"` - Type uint8 `gorm:"type:tinyint(1);not null;comment:Type: 1: Recharge 2: Withdraw 3: Payment 4: Refund 5: Reward"` - OrderId int64 `gorm:"default:null;comment:Order ID"` - Balance int64 `gorm:"not null;comment:Balance"` - CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` -} - -func (BalanceLog) TableName() string { - return "user_balance_log" -} - -type GiftAmountLog struct { - Id int64 `gorm:"primaryKey"` - UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"` - UserSubscribeId int64 `gorm:"default:null;comment:Deduction User Subscribe ID"` - OrderNo string `gorm:"default:null;comment:Order No."` - Type uint8 `gorm:"type:tinyint(1);not null;comment:Type: 1: Increase 2: Reduce"` - Amount int64 `gorm:"not null;comment:Amount"` - Balance int64 `gorm:"not null;comment:Balance"` - Remark string `gorm:"type:varchar(255);default:'';comment:Remark"` - CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` -} - -func (GiftAmountLog) TableName() string { - return "user_gift_amount_log" -} - -type CommissionLog struct { - Id int64 `gorm:"primaryKey"` - UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"` - OrderNo string `gorm:"default:null;comment:Order No."` - Amount int64 `gorm:"not null;comment:Amount"` - CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` -} - -func (CommissionLog) TableName() string { - return "user_commission_log" -} - type AuthMethods struct { Id int64 `gorm:"primaryKey"` UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"` @@ -139,7 +68,7 @@ type AuthMethods struct { UpdatedAt time.Time `gorm:"comment:Update Time"` } -func (AuthMethods) TableName() string { +func (*AuthMethods) TableName() string { return "user_auth_methods" } @@ -155,7 +84,7 @@ type Device struct { UpdatedAt time.Time `gorm:"comment:Update Time"` } -func (Device) TableName() string { +func (*Device) TableName() string { return "user_device" } @@ -174,57 +103,17 @@ func (DeviceOnlineRecord) TableName() string { return "user_device_online_record" } -type LoginLog struct { +type Withdrawal struct { Id int64 `gorm:"primaryKey"` UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"` - LoginIP string `gorm:"type:varchar(255);not null;comment:Login IP"` - UserAgent string `gorm:"type:text;not null;comment:UserAgent"` - Success *bool `gorm:"default:false;not null;comment:Login Success"` + Amount int64 `gorm:"not null;comment:Withdrawal Amount"` + Content string `gorm:"type:text;comment:Withdrawal Content"` + Status uint8 `gorm:"type:tinyint(1);default:0;comment:Withdrawal Status: 0: Pending 1: Approved 2: Rejected"` + Reason string `gorm:"type:varchar(500);default:'';comment:Rejection Reason"` CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` } -func (LoginLog) TableName() string { - return "user_login_log" -} - -type SubscribeLog struct { - Id int64 `gorm:"primaryKey"` - UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"` - UserSubscribeId int64 `gorm:"index:idx_user_subscribe_id;not null;comment:User Subscribe ID"` - Token string `gorm:"type:varchar(255);not null;comment:Token"` - IP string `gorm:"type:varchar(255);not null;comment:IP"` - UserAgent string `gorm:"type:text;not null;comment:UserAgent"` - CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` -} - -func (SubscribeLog) TableName() string { - return "user_subscribe_log" -} - -const ( - ResetSubscribeTypeAuto uint8 = 1 - ResetSubscribeTypeAdvance uint8 = 2 - ResetSubscribeTypePaid uint8 = 3 -) - -type FilterResetSubscribeLogParams struct { - Page int - Size int - Type uint8 - UserId int64 - OrderNo string - UserSubscribeId int64 -} - -type ResetSubscribeLog struct { - Id int64 `gorm:"primaryKey"` - UserId int64 `gorm:"type:bigint;index:idx_user_id;not null;comment:User ID"` - Type uint8 `gorm:"type:tinyint(1);not null;comment:Type: 1: Auto 2: Advance 3: Paid"` - OrderNo string `gorm:"type:varchar(255);default:null;comment:Order No."` - UserSubscribeId int64 `gorm:"type:bigint;index:idx_user_subscribe_id;not null;comment:User Subscribe ID"` - CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` -} - -func (ResetSubscribeLog) TableName() string { - return "user_reset_subscribe_log" +func (*Withdrawal) TableName() string { + return "user_withdrawal" } diff --git a/internal/report/report.go b/internal/report/report.go new file mode 100644 index 0000000..1ba618a --- /dev/null +++ b/internal/report/report.go @@ -0,0 +1,15 @@ +package report + +const ( + RegisterAPI = "/basic/register" // 模块注册接口 +) + +// RegisterResponse 模块注册响应参数 +type RegisterResponse struct { + Code int `json:"code"` // 响应代码 + Message string `json:"message"` // 响应信息 + Data struct { + Success bool `json:"success"` // 注册是否成功 + Message string `json:"message"` // 返回信息 + } `json:"data"` // 响应数据 +} diff --git a/internal/report/tool.go b/internal/report/tool.go new file mode 100644 index 0000000..fe8a68c --- /dev/null +++ b/internal/report/tool.go @@ -0,0 +1,113 @@ +package report + +import ( + "fmt" + "net" + "os" + + "github.com/go-resty/resty/v2" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/logger" + "github.com/pkg/errors" +) + +// FreePort returns a free TCP port by opening a listener on port 0. +func FreePort() (int, error) { + l, err := net.Listen("tcp", ":0") + if err != nil { + return 0, err + } + defer l.Close() + // Get the assigned port + addr := l.Addr().(*net.TCPAddr) + return addr.Port, nil +} + +// ModulePort returns the module port from the environment variable or a free port. +func ModulePort() (int, error) { + // 从环境变量获取端口号 + value, exists := os.LookupEnv("PPANEL_PORT") + if exists { + var port int + _, err := fmt.Sscanf(value, "%d", &port) + if err != nil { + return FreePort() + } + return port, nil + } + return FreePort() +} + +// GatewayPort returns the gateway port from the environment variable or a free port. +func GatewayPort() (int, error) { + // 从环境变量获取端口号 + value, exists := os.LookupEnv("GATEWAY_PORT") + if exists { + var port int + _, err := fmt.Sscanf(value, "%d", &port) + if err != nil { + logger.Errorf("Failed to parse GATEWAY_PORT: %v Value %s", err.Error(), value) + panic(err) + } + return port, nil + } + return 0, errors.New("could not determine gateway port") +} + +// RegisterModule registers a module with the gateway. +func RegisterModule(port int) error { + // 从环境变量中读取网关模块端口 + gatewayPort, err := GatewayPort() + if err != nil { + logger.Errorf("Failed to determine GATEWAY_PORT: %v", err) + return err + } + + // 从环境变量中获取通讯密钥 + value, exists := os.LookupEnv("SECRET_KEY") + if !exists { + panic("could not determine secret key") + } + + var response RegisterResponse + + client := resty.New().SetBaseURL(fmt.Sprintf("http://127.0.0.1:%d", gatewayPort)) + result, err := client.R().SetHeader("Content-Type", "application/json").SetBody(RegisterServiceRequest{ + Secret: value, + ProxyPath: "/api", + ServiceURL: fmt.Sprintf("http://127.0.0.1:%d", port), + Repository: constant.Repository, + HeartbeatURL: fmt.Sprintf("http://127.0.0.1:%d/v1/common/heartbeat", port), + ServiceName: constant.ServiceName, + ServiceVersion: constant.Version, + }).SetResult(&response).Post(RegisterAPI) + + if err != nil { + logger.Errorf("Failed to register service: %v", err) + return err + } + + if result.IsError() { + return errors.New("failed to register module: " + result.Status()) + } + + if !response.Data.Success { + logger.Infof("Result: %v", result.String()) + return errors.New("failed to register module: " + response.Message) + } + logger.Infof("Module registered successfully: %s", response.Message) + return nil +} + +// IsGatewayMode checks if the application is running in gateway mode. +// It returns true if GATEWAY_MODE is set to "true" and GATEWAY_PORT is valid. +func IsGatewayMode() bool { + value, exists := os.LookupEnv("GATEWAY_MODE") + if exists && value == "true" { + if _, err := GatewayPort(); err == nil { + return true + } + } + + return false +} diff --git a/internal/report/tool_test.go b/internal/report/tool_test.go new file mode 100644 index 0000000..cee9d83 --- /dev/null +++ b/internal/report/tool_test.go @@ -0,0 +1,21 @@ +package report + +import ( + "testing" +) + +func TestFreePort(t *testing.T) { + port, err := FreePort() + if err != nil { + t.Fatalf("FreePort() error: %v", err) + } + t.Logf("FreePort: %v", port) +} + +func TestModulePort(t *testing.T) { + port, err := ModulePort() + if err != nil { + t.Fatalf("ModulePort() error: %v", err) + } + t.Logf("ModulePort: %v", port) +} diff --git a/internal/report/types.go b/internal/report/types.go new file mode 100644 index 0000000..d9cd643 --- /dev/null +++ b/internal/report/types.go @@ -0,0 +1,11 @@ +package report + +type RegisterServiceRequest struct { + Secret string `json:"secret"` // 通讯密钥 + ProxyPath string `json:"proxy_path"` // 代理路径 + ServiceURL string `json:"service_url"` // 服务地址 + Repository string `json:"repository"` // 服务代码仓库 + ServiceName string `json:"service_name"` // 服务名称 + HeartbeatURL string `json:"heartbeat_url"` // 心跳检测地址 + ServiceVersion string `json:"service_version"` // 服务版本 +} diff --git a/internal/server.go b/internal/server.go index 64c2704..78d6422 100644 --- a/internal/server.go +++ b/internal/server.go @@ -6,8 +6,10 @@ import ( "errors" "fmt" "net/http" + "os" "time" + "github.com/perfect-panel/server/internal/report" "github.com/perfect-panel/server/pkg/logger" "github.com/perfect-panel/server/pkg/proc" @@ -48,7 +50,7 @@ func initServer(svc *svc.ServiceContext) *gin.Engine { } r.Use(sessions.Sessions("ppanel", sessionStore)) // use cors middleware - r.Use(middleware.TraceMiddleware(svc), middleware.LoggerMiddleware(svc), middleware.CorsMiddleware, middleware.PanDomainMiddleware(svc), gin.Recovery()) + r.Use(middleware.TraceMiddleware(svc), middleware.LoggerMiddleware(svc), middleware.CorsMiddleware, gin.Recovery()) // register handlers handler.RegisterHandlers(r, svc) @@ -65,9 +67,32 @@ func (m *Service) Start() { if m.svc == nil { panic("config file path is nil") } + // init service r := initServer(m.svc) - serverAddr := fmt.Sprintf("%v:%d", m.svc.Config.Host, m.svc.Config.Port) + // get server port + port := m.svc.Config.Port + host := m.svc.Config.Host + // check gateway mode + if report.IsGatewayMode() { + // get free port + freePort, err := report.ModulePort() + if err != nil { + logger.Errorf("get module port error: %s", err.Error()) + panic(err) + } + port = freePort + host = "127.0.0.1" + // register module + err = report.RegisterModule(port) + if err != nil { + logger.Errorf("register module error: %s", err.Error()) + os.Exit(1) + } + logger.Infof("module registered on port %d", port) + } + + serverAddr := fmt.Sprintf("%v:%d", host, port) m.server = &http.Server{ Addr: serverAddr, Handler: r, diff --git a/internal/svc/mmdb.go b/internal/svc/mmdb.go new file mode 100644 index 0000000..331034f --- /dev/null +++ b/internal/svc/mmdb.go @@ -0,0 +1,74 @@ +package svc + +import ( + "io" + "net/http" + "os" + "path/filepath" + + "github.com/oschwald/geoip2-golang" + "github.com/perfect-panel/server/pkg/logger" +) + +const GeoIPDBURL = "https://raw.githubusercontent.com/adysec/IP_database/main/geolite/GeoLite2-City.mmdb" + +type IPLocation struct { + Path string + DB *geoip2.Reader +} + +func NewIPLocation(path string) (*IPLocation, error) { + + // 检查文件是否存在 + if _, err := os.Stat(path); os.IsNotExist(err) { + logger.Infof("[GeoIP] Database not found, downloading from %s", GeoIPDBURL) + // 文件不存在,下载数据库 + err := DownloadGeoIPDatabase(GeoIPDBURL, path) + if err != nil { + logger.Errorf("[GeoIP] Failed to download database: %v", err.Error()) + return nil, err + } + logger.Infof("[GeoIP] Database downloaded successfully") + } + + db, err := geoip2.Open(path) + if err != nil { + return nil, err + } + return &IPLocation{ + Path: path, + DB: db, + }, nil +} + +func (ipLoc *IPLocation) Close() error { + return ipLoc.DB.Close() +} + +func DownloadGeoIPDatabase(url, path string) error { + + // 创建路径, 确保目录存在 + err := os.MkdirAll(filepath.Dir(path), 0755) + if err != nil { + logger.Errorf("[GeoIP] Failed to create directory: %v", err.Error()) + return err + } + + // 创建文件 + out, err := os.Create(path) + if err != nil { + return err + } + defer out.Close() + + // 请求远程文件 + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + // 保存文件 + _, err = io.Copy(out, resp.Body) + return err +} diff --git a/internal/svc/serviceContext.go b/internal/svc/serviceContext.go index 3854e42..4f6bc3a 100644 --- a/internal/svc/serviceContext.go +++ b/internal/svc/serviceContext.go @@ -3,25 +3,20 @@ package svc import ( "context" + "github.com/perfect-panel/server/internal/model/client" + "github.com/perfect-panel/server/internal/model/node" "github.com/perfect-panel/server/pkg/device" - "github.com/perfect-panel/server/internal/model/ads" - "github.com/perfect-panel/server/internal/model/cache" - - tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" - "github.com/hibiken/asynq" "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/model/ads" "github.com/perfect-panel/server/internal/model/announcement" - "github.com/perfect-panel/server/internal/model/application" "github.com/perfect-panel/server/internal/model/auth" "github.com/perfect-panel/server/internal/model/coupon" "github.com/perfect-panel/server/internal/model/document" "github.com/perfect-panel/server/internal/model/log" "github.com/perfect-panel/server/internal/model/order" "github.com/perfect-panel/server/internal/model/payment" - "github.com/perfect-panel/server/internal/model/server" "github.com/perfect-panel/server/internal/model/subscribe" - "github.com/perfect-panel/server/internal/model/subscribeType" "github.com/perfect-panel/server/internal/model/system" "github.com/perfect-panel/server/internal/model/ticket" "github.com/perfect-panel/server/internal/model/traffic" @@ -29,32 +24,39 @@ import ( "github.com/perfect-panel/server/pkg/limit" "github.com/perfect-panel/server/pkg/nodeMultiplier" "github.com/perfect-panel/server/pkg/orm" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + "github.com/hibiken/asynq" "github.com/redis/go-redis/v9" "gorm.io/gorm" ) type ServiceContext struct { - DB *gorm.DB - Redis *redis.Client - Config config.Config - Queue *asynq.Client - NodeCache *cache.NodeCacheClient - AuthModel auth.Model - AdsModel ads.Model - LogModel log.Model - UserModel user.Model - OrderModel order.Model - TicketModel ticket.Model - ServerModel server.Model - SystemModel system.Model - CouponModel coupon.Model - PaymentModel payment.Model - DocumentModel document.Model - SubscribeModel subscribe.Model - TrafficLogModel traffic.Model - ApplicationModel application.Model - AnnouncementModel announcement.Model - SubscribeTypeModel subscribeType.Model + DB *gorm.DB + Redis *redis.Client + Config config.Config + Queue *asynq.Client + ExchangeRate float64 + GeoIP *IPLocation + + //NodeCache *cache.NodeCacheClient + AuthModel auth.Model + AdsModel ads.Model + LogModel log.Model + NodeModel node.Model + UserModel user.Model + OrderModel order.Model + ClientModel client.Model + TicketModel ticket.Model + //ServerModel server.Model + SystemModel system.Model + CouponModel coupon.Model + PaymentModel payment.Model + DocumentModel document.Model + SubscribeModel subscribe.Model + TrafficLogModel traffic.Model + AnnouncementModel announcement.Model + Restart func() error TelegramBot *tgbotapi.BotAPI NodeMultiplierManager *nodeMultiplier.Manager @@ -67,9 +69,17 @@ func NewServiceContext(c config.Config) *ServiceContext { db, err := orm.ConnectMysql(orm.Mysql{ Config: c.MySQL, }) + if err != nil { panic(err.Error()) } + + // IP location initialize + geoIP, err := NewIPLocation("./cache/GeoLite2-City.mmdb") + if err != nil { + panic(err.Error()) + } + rds := redis.NewClient(&redis.Options{ Addr: c.Redis.Host, Password: c.Redis.Pass, @@ -83,26 +93,29 @@ func NewServiceContext(c config.Config) *ServiceContext { } authLimiter := limit.NewPeriodLimit(86400, 15, rds, config.SendCountLimitKeyPrefix, limit.Align()) srv := &ServiceContext{ - DB: db, - Redis: rds, - Config: c, - Queue: NewAsynqClient(c), - NodeCache: cache.NewNodeCacheClient(rds), - AuthLimiter: authLimiter, - AdsModel: ads.NewModel(db, rds), - LogModel: log.NewModel(db), - AuthModel: auth.NewModel(db, rds), - UserModel: user.NewModel(db, rds), - OrderModel: order.NewModel(db, rds), - TicketModel: ticket.NewModel(db, rds), - ServerModel: server.NewModel(db, rds), + DB: db, + Redis: rds, + Config: c, + Queue: NewAsynqClient(c), + ExchangeRate: 1.0, + GeoIP: geoIP, + //NodeCache: cache.NewNodeCacheClient(rds), + AuthLimiter: authLimiter, + AdsModel: ads.NewModel(db, rds), + LogModel: log.NewModel(db), + NodeModel: node.NewModel(db, rds), + AuthModel: auth.NewModel(db, rds), + UserModel: user.NewModel(db, rds), + OrderModel: order.NewModel(db, rds), + ClientModel: client.NewSubscribeApplicationModel(db), + TicketModel: ticket.NewModel(db, rds), + //ServerModel: server.NewModel(db, rds), SystemModel: system.NewModel(db, rds), CouponModel: coupon.NewModel(db, rds), PaymentModel: payment.NewModel(db, rds), DocumentModel: document.NewModel(db, rds), SubscribeModel: subscribe.NewModel(db, rds), TrafficLogModel: traffic.NewModel(db), - ApplicationModel: application.NewModel(db, rds), AnnouncementModel: announcement.NewModel(db, rds), } srv.DeviceManager = NewDeviceManager(srv) diff --git a/internal/types/types.go b/internal/types/types.go index 80f5fc1..565c4ce 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -37,80 +37,6 @@ type AnyTLS struct { SecurityConfig SecurityConfig `json:"security_config"` } -type AppAuthCheckRequest struct { - Method string `json:"method" validate:"required" validate:"required,oneof=device email mobile"` - Account string `json:"account"` - Identifier string `json:"identifier" validate:"required"` - UserAgent string `json:"user_agent" validate:"required,oneof=windows mac linux android ios harmony"` - AreaCode string `json:"area_code"` -} - -type AppAuthCheckResponse struct { - Status bool -} - -type AppAuthRequest struct { - Method string `json:"method" validate:"required" validate:"required,oneof=device email mobile"` - Account string `json:"account"` - Password string `json:"password"` - Identifier string `json:"identifier" validate:"required"` - UserAgent string `json:"user_agent" validate:"required,oneof=windows mac linux android ios harmony"` - Code string `json:"code"` - Invite string `json:"invite"` - AreaCode string `json:"area_code"` - CfToken string `json:"cf_token,optional"` -} - -type AppAuthRespone struct { - Token string `json:"token"` -} - -type AppConfigRequest struct { - UserAgent string `json:"user_agent" validate:"required,oneof=windows mac linux android ios harmony"` -} - -type AppConfigResponse struct { - EncryptionKey string `json:"encryption_key"` - EncryptionMethod string `json:"encryption_method"` - Domains []string `json:"domains"` - StartupPicture string `json:"startup_picture"` - StartupPictureSkipTime int64 `json:"startup_picture_skip_time"` - Application AppInfo `json:"applications"` - OfficialEmail string `json:"official_email"` - OfficialWebsite string `json:"official_website"` - OfficialTelegram string `json:"official_telegram"` - OfficialTelephone string `json:"official_telephone"` - InvitationLink string `json:"invitation_link"` - KrWebsiteId string `json:"kr_website_id"` -} - -type AppInfo struct { - Id int64 `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - Url string `json:"url"` - Version string `json:"version"` - VersionDescription string `json:"version_description"` - IsDefault bool `json:"is_default"` -} - -type AppRuleGroupListResponse struct { - Total int64 `json:"total"` - List []ServerRuleGroup `json:"list"` -} - -type AppSendCodeRequest struct { - Method string `json:"method" validate:"required" validate:"required,oneof=email mobile"` - Account string `json:"account"` - AreaCode string `json:"area_code"` - CfToken string `json:"cf_token,optional"` -} - -type AppSendCodeRespone struct { - Status bool `json:"status"` - Code string `json:"code,omitempty"` -} - type AppUserSubcbribe struct { Id int64 `json:"id"` Name string `json:"name"` @@ -145,22 +71,6 @@ type AppUserSubscbribeNode struct { Download int64 `json:"download"` } -type AppUserSubscbribeNodeRequest struct { - Id int64 `form:"id" validate:"required"` -} - -type AppUserSubscbribeNodeResponse struct { - List []AppUserSubscbribeNode `json:"list"` -} - -type AppUserSubscbribeResponse struct { - List []AppUserSubcbribe `json:"list"` -} - -type AppUserSubscribeRequest struct { - ContainsNodes *bool `form:"contains_nodes"` -} - type AppleLoginCallbackRequest struct { Code string `form:"code"` IDToken string `form:"id_token"` @@ -175,15 +85,6 @@ type Application struct { SubscribeType string `json:"subscribe_type"` } -type ApplicationConfig struct { - AppId int64 `json:"app_id"` - EncryptionKey string `json:"encryption_key"` - EncryptionMethod string `json:"encryption_method"` - Domains []string `json:"domains" validate:"required"` - StartupPicture string `json:"startup_picture"` - StartupPictureSkipTime int64 `json:"startup_picture_skip_time"` -} - type ApplicationPlatform struct { IOS []*ApplicationVersion `json:"ios,omitempty"` MacOS []*ApplicationVersion `json:"macos,omitempty"` @@ -217,6 +118,7 @@ type ApplicationVersion struct { type AuthConfig struct { Mobile MobileAuthenticateConfig `json:"mobile"` Email EmailAuthticateConfig `json:"email"` + Device DeviceAuthticateConfig `json:"device"` Register PubilcRegisterConfig `json:"register"` } @@ -227,6 +129,15 @@ type AuthMethodConfig struct { Enabled bool `json:"enabled"` } +type BalanceLog struct { + Type uint16 `json:"type"` + UserId int64 `json:"user_id"` + Amount int64 `json:"amount"` + OrderNo string `json:"order_no,omitempty"` + Balance int64 `json:"balance"` + Timestamp int64 `json:"timestamp"` +} + type BatchDeleteCouponRequest struct { Ids []int64 `json:"ids" validate:"required"` } @@ -235,14 +146,6 @@ type BatchDeleteDocumentRequest struct { Ids []int64 `json:"ids" validate:"required"` } -type BatchDeleteNodeGroupRequest struct { - Ids []int64 `json:"ids" validate:"required"` -} - -type BatchDeleteNodeRequest struct { - Ids []int64 `json:"ids" validate:"required"` -} - type BatchDeleteSubscribeGroupRequest struct { Ids []int64 `json:"ids" validate:"required"` } @@ -255,6 +158,26 @@ type BatchDeleteUserRequest struct { Ids []int64 `json:"ids" validate:"required"` } +type BatchSendEmailTask struct { + Id int64 `json:"id"` + Subject string `json:"subject"` + Content string `json:"content"` + Recipients string `json:"recipients"` + Scope int8 `json:"scope"` + RegisterStartTime int64 `json:"register_start_time"` + RegisterEndTime int64 `json:"register_end_time"` + Additional string `json:"additional"` + Scheduled int64 `json:"scheduled"` + Interval uint8 `json:"interval"` + Limit uint64 `json:"limit"` + Status uint8 `json:"status"` + Errors string `json:"errors"` + Total uint64 `json:"total"` + Current uint64 `json:"current"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + type BindOAuthCallbackRequest struct { Method string `json:"method"` Callback interface{} `json:"callback"` @@ -309,17 +232,16 @@ type CloseOrderRequest struct { } type CommissionLog struct { - Id int64 `json:"id"` + Type uint16 `json:"type"` UserId int64 `json:"user_id"` - OrderNo string `json:"order_no"` Amount int64 `json:"amount"` - CreatedAt int64 `json:"created_at"` + OrderNo string `json:"order_no"` + Timestamp int64 `json:"timestamp"` } -type ConnectionRecords struct { - CurrentContinuousDays int64 `json:"current_continuous_days"` - HistoryContinuousDays int64 `json:"history_continuous_days"` - LongestSingleConnection int64 `json:"longest_single_connection"` +type CommissionWithdrawRequest struct { + Amount int64 `json:"amount"` + Content string `json:"content"` } type Coupon struct { @@ -355,21 +277,16 @@ type CreateAnnouncementRequest struct { Content string `json:"content" validate:"required"` } -type CreateApplicationRequest struct { - Icon string `json:"icon"` - Name string `json:"name"` - Description string `json:"description"` - SubscribeType string `json:"subscribe_type"` - Platform ApplicationPlatform `json:"platform"` -} - -type CreateApplicationVersionRequest struct { - Url string `json:"url"` - Version string `json:"version" validate:"required"` - Description string `json:"description"` - Platform string `json:"platform" validate:"required,oneof=windows mac linux android ios harmony"` - IsDefault bool `json:"is_default"` - ApplicationId int64 `json:"application_id" validate:"required"` +type CreateBatchSendEmailTaskRequest struct { + Subject string `json:"subject"` + Content string `json:"content"` + Scope int8 `json:"scope"` + RegisterStartTime int64 `json:"register_start_time,omitempty"` + RegisterEndTime int64 `json:"register_end_time,omitempty"` + Additional string `json:"additional,omitempty"` + Scheduled int64 `json:"scheduled,omitempty"` + Interval uint8 `json:"interval,omitempty"` + Limit uint64 `json:"limit,omitempty"` } type CreateCouponRequest struct { @@ -393,26 +310,14 @@ type CreateDocumentRequest struct { Show *bool `json:"show"` } -type CreateNodeGroupRequest struct { - Name string `json:"name" validate:"required"` - Description string `json:"description"` -} - type CreateNodeRequest struct { - Name string `json:"name" validate:"required"` - Tags []string `json:"tags"` - Country string `json:"country"` - City string `json:"city"` - ServerAddr string `json:"server_addr" validate:"required"` - RelayMode string `json:"relay_mode"` - RelayNode []NodeRelay `json:"relay_node"` - SpeedLimit int `json:"speed_limit"` - TrafficRatio float32 `json:"traffic_ratio"` - GroupId int64 `json:"group_id"` - Protocol string `json:"protocol" validate:"required"` - Config interface{} `json:"config" validate:"required"` - Enable *bool `json:"enable"` - Sort int64 `json:"sort"` + Name string `json:"name"` + Tags []string `json:"tags,omitempty"` + Port uint16 `json:"port"` + Address string `json:"address"` + ServerId int64 `json:"server_id"` + Protocol string `json:"protocol"` + Enabled *bool `json:"enabled"` } type CreateOrderRequest struct { @@ -445,14 +350,36 @@ type CreatePaymentMethodRequest struct { Enable *bool `json:"enable" validate:"required"` } -type CreateRuleGroupRequest struct { - Name string `json:"name" validate:"required"` - Icon string `json:"icon"` - Type string `json:"type"` - Tags []string `json:"tags"` - Rules string `json:"rules"` - Default bool `json:"default"` - Enable bool `json:"enable"` +type CreateQuotaTaskRequest struct { + Subscribers []int64 `json:"subscribers"` + IsActive *bool `json:"is_active"` + StartTime int64 `json:"start_time"` + EndTime int64 `json:"end_time"` + ResetTraffic bool `json:"reset_traffic"` + Days uint64 `json:"days"` + GiftType uint8 `json:"gift_type"` + GiftValue uint64 `json:"gift_value"` +} + +type CreateServerRequest struct { + Name string `json:"name"` + Country string `json:"country,omitempty"` + City string `json:"city,omitempty"` + Address string `json:"address"` + Sort int `json:"sort,omitempty"` + Protocols []Protocol `json:"protocols"` +} + +type CreateSubscribeApplicationRequest struct { + Name string `json:"name"` + Description string `json:"description,omitempty"` + Icon string `json:"icon,omitempty"` + Scheme string `json:"scheme,omitempty"` + UserAgent string `json:"user_agent"` + IsDefault bool `json:"is_default"` + SubscribeTemplate string `json:"template"` + OutputFormat string `json:"output_format"` + DownloadLink DownloadLink `json:"download_link"` } type CreateSubscribeGroupRequest struct { @@ -462,6 +389,7 @@ type CreateSubscribeGroupRequest struct { type CreateSubscribeRequest struct { Name string `json:"name" validate:"required"` + Language string `json:"language"` Description string `json:"description"` UnitPrice int64 `json:"unit_price"` UnitTime string `json:"unit_time"` @@ -472,10 +400,8 @@ type CreateSubscribeRequest struct { SpeedLimit int64 `json:"speed_limit"` DeviceLimit int64 `json:"device_limit"` Quota int64 `json:"quota"` - GroupId int64 `json:"group_id"` - ServerGroup []int64 `json:"server_group"` - Server []int64 `json:"server"` - ServerCount int64 `json:"server_count"` + Nodes []int64 `json:"nodes"` + NodeTags []string `json:"node_tags"` Show *bool `json:"show"` Sell *bool `json:"sell"` DeductionRatio int64 `json:"deduction_ratio"` @@ -498,18 +424,20 @@ type CreateUserAuthMethodRequest struct { } type CreateUserRequest struct { - Email string `json:"email"` - Telephone string `json:"telephone"` - TelephoneAreaCode string `json:"telephone_area_code"` - Password string `json:"password"` - ProductId int64 `json:"product_id"` - Duration int64 `json:"duration"` - RefererUser string `json:"referer_user"` - ReferCode string `json:"refer_code"` - Balance int64 `json:"balance"` - Commission int64 `json:"commission"` - GiftAmount int64 `json:"gift_amount"` - IsAdmin bool `json:"is_admin"` + Email string `json:"email"` + Telephone string `json:"telephone"` + TelephoneAreaCode string `json:"telephone_area_code"` + Password string `json:"password"` + ProductId int64 `json:"product_id"` + Duration int64 `json:"duration"` + ReferralPercentage uint8 `json:"referral_percentage"` + OnlyFirstPurchase bool `json:"only_first_purchase"` + RefererUser string `json:"referer_user"` + ReferCode string `json:"refer_code"` + Balance int64 `json:"balance"` + Commission int64 `json:"commission"` + GiftAmount int64 `json:"gift_amount"` + IsAdmin bool `json:"is_admin"` } type CreateUserSubscribeRequest struct { @@ -542,11 +470,6 @@ type CurrencyConfig struct { CurrencySymbol string `json:"currency_symbol"` } -type DeleteAccountRequest struct { - Method string `json:"method" validate:"required" validate:"required,oneof=email telephone device"` - Code string `json:"code"` -} - type DeleteAdsRequest struct { Id int64 `json:"id"` } @@ -555,14 +478,6 @@ type DeleteAnnouncementRequest struct { Id int64 `json:"id" validate:"required"` } -type DeleteApplicationRequest struct { - Id int64 `json:"id" validate:"required"` -} - -type DeleteApplicationVersionRequest struct { - Id int64 `json:"id" validate:"required"` -} - type DeleteCouponRequest struct { Id int64 `json:"id" validate:"required"` } @@ -571,20 +486,20 @@ type DeleteDocumentRequest struct { Id int64 `json:"id" validate:"required"` } -type DeleteNodeGroupRequest struct { - Id int64 `json:"id" validate:"required"` -} - type DeleteNodeRequest struct { - Id int64 `json:"id" validate:"required"` + Id int64 `json:"id"` } type DeletePaymentMethodRequest struct { Id int64 `json:"id" validate:"required"` } -type DeleteRuleGroupRequest struct { - Id int64 `json:"id" validate:"required"` +type DeleteServerRequest struct { + Id int64 `json:"id"` +} + +type DeleteSubscribeApplicationRequest struct { + Id int64 `json:"id"` } type DeleteSubscribeGroupRequest struct { @@ -608,6 +523,20 @@ type DeleteUserSubscribeRequest struct { UserSubscribeId int64 `json:"user_subscribe_id"` } +type DeviceAuthticateConfig struct { + Enable bool `json:"enable"` + ShowAds bool `json:"show_ads"` + EnableSecurity bool `json:"enable_security"` + OnlyRealDevice bool `json:"only_real_device"` +} + +type DeviceLoginRequest struct { + Identifier string `json:"identifier" validate:"required"` + IP string `header:"X-Original-Forwarded-For"` + UserAgent string `json:"user_agent" validate:"required"` + CfToken string `json:"cf_token,optional"` +} + type Document struct { Id int64 `json:"id"` Title string `json:"title"` @@ -618,6 +547,15 @@ type Document struct { UpdatedAt int64 `json:"updated_at"` } +type DownloadLink struct { + IOS string `json:"ios,omitempty"` + Android string `json:"android,omitempty"` + Windows string `json:"windows,omitempty"` + Mac string `json:"mac,omitempty"` + Linux string `json:"linux,omitempty"` + Harmony string `json:"harmony,omitempty"` +} + type EPayNotifyRequest struct { Pid int64 `json:"pid" form:"pid"` TradeNo string `json:"trade_no" form:"trade_no"` @@ -638,6 +576,149 @@ type EmailAuthticateConfig struct { DomainSuffixList string `json:"domain_suffix_list"` } +type FilterBalanceLogRequest struct { + FilterLogParams + UserId int64 `form:"user_id,optional"` +} + +type FilterBalanceLogResponse struct { + Total int64 `json:"total"` + List []BalanceLog `json:"list"` +} + +type FilterCommissionLogRequest struct { + FilterLogParams + UserId int64 `form:"user_id,optional"` +} + +type FilterCommissionLogResponse struct { + Total int64 `json:"total"` + List []CommissionLog `json:"list"` +} + +type FilterEmailLogResponse struct { + Total int64 `json:"total"` + List []MessageLog `json:"list"` +} + +type FilterGiftLogRequest struct { + FilterLogParams + UserId int64 `form:"user_id,optional"` +} + +type FilterGiftLogResponse struct { + Total int64 `json:"total"` + List []GiftLog `json:"list"` +} + +type FilterLogParams struct { + Page int `form:"page"` + Size int `form:"size"` + Date string `form:"date,optional"` + Search string `form:"search,optional"` +} + +type FilterLoginLogRequest struct { + FilterLogParams + UserId int64 `form:"user_id,optional"` +} + +type FilterLoginLogResponse struct { + Total int64 `json:"total"` + List []LoginLog `json:"list"` +} + +type FilterMobileLogResponse struct { + Total int64 `json:"total"` + List []MessageLog `json:"list"` +} + +type FilterNodeListRequest struct { + Page int `form:"page"` + Size int `form:"size"` + Search string `form:"search,omitempty"` +} + +type FilterNodeListResponse struct { + Total int64 `json:"total"` + List []Node `json:"list"` +} + +type FilterRegisterLogRequest struct { + FilterLogParams + UserId int64 `form:"user_id,optional"` +} + +type FilterRegisterLogResponse struct { + Total int64 `json:"total"` + List []RegisterLog `json:"list"` +} + +type FilterResetSubscribeLogRequest struct { + FilterLogParams + UserSubscribeId int64 `form:"user_subscribe_id,optional"` +} + +type FilterResetSubscribeLogResponse struct { + Total int64 `json:"total"` + List []ResetSubscribeLog `json:"list"` +} + +type FilterServerListRequest struct { + Page int `form:"page"` + Size int `form:"size"` + Search string `form:"search,omitempty"` +} + +type FilterServerListResponse struct { + Total int64 `json:"total"` + List []Server `json:"list"` +} + +type FilterServerTrafficLogRequest struct { + FilterLogParams + ServerId int64 `form:"server_id,optional"` +} + +type FilterServerTrafficLogResponse struct { + Total int64 `json:"total"` + List []ServerTrafficLog `json:"list"` +} + +type FilterSubscribeLogRequest struct { + FilterLogParams + UserId int64 `form:"user_id,optional"` + UserSubscribeId int64 `form:"user_subscribe_id,optional"` +} + +type FilterSubscribeLogResponse struct { + Total int64 `json:"total"` + List []SubscribeLog `json:"list"` +} + +type FilterSubscribeTrafficRequest struct { + FilterLogParams + UserId int64 `form:"user_id,optional"` + UserSubscribeId int64 `form:"user_subscribe_id,optional"` +} + +type FilterSubscribeTrafficResponse struct { + Total int64 `json:"total"` + List []UserSubscribeTrafficLog `json:"list"` +} + +type FilterTrafficLogDetailsRequest struct { + FilterLogParams + ServerId int64 `form:"server_id,optional"` + SubscribeId int64 `form:"subscribe_id,optional"` + UserId int64 `form:"user_id,optional"` +} + +type FilterTrafficLogDetailsResponse struct { + Total int64 `json:"total"` + List []TrafficLogDetails `json:"list"` +} + type Follow struct { Id int64 `json:"id"` TicketId int64 `json:"ticket_id"` @@ -690,11 +771,6 @@ type GetAnnouncementRequest struct { Id int64 `form:"id" validate:"required"` } -type GetAppcationResponse struct { - Config ApplicationConfig `json:"config"` - Applications []ApplicationResponseInfo `json:"applications"` -} - type GetAuthMethodConfigRequest struct { Method string `form:"method"` } @@ -707,6 +783,29 @@ type GetAvailablePaymentMethodsResponse struct { List []PaymentMethod `json:"list"` } +type GetBatchSendEmailTaskListRequest struct { + Page int `form:"page"` + Size int `form:"size"` + Scope *int8 `form:"scope,omitempty"` + Status *uint8 `form:"status,omitempty"` +} + +type GetBatchSendEmailTaskListResponse struct { + Total int64 `json:"total"` + List []BatchSendEmailTask `json:"list"` +} + +type GetBatchSendEmailTaskStatusRequest struct { + Id int64 `json:"id"` +} + +type GetBatchSendEmailTaskStatusResponse struct { + Status uint8 `json:"status"` + Current int64 `json:"current"` + Total int64 `json:"total"` + Errors string `json:"errors"` +} + type GetCouponListRequest struct { Page int64 `form:"page" validate:"required"` Size int64 `form:"size" validate:"required"` @@ -723,6 +822,11 @@ type GetDetailRequest struct { Id int64 `form:"id" validate:"required"` } +type GetDeviceListResponse struct { + List []UserDevice `json:"list"` + Total int64 `json:"total"` +} + type GetDocumentDetailRequest struct { Id int64 `json:"id" validate:"required"` } @@ -762,14 +866,10 @@ type GetLoginLogResponse struct { } type GetMessageLogListRequest struct { - Page int `form:"page"` - Size int `form:"size"` - Type string `form:"type"` - Platform string `form:"platform,omitempty"` - To string `form:"to,omitempty"` - Subject string `form:"subject,omitempty"` - Content string `form:"content,omitempty"` - Status int `form:"status,omitempty"` + Page int `form:"page"` + Size int `form:"size"` + Type uint8 `form:"type"` + Search string `form:"search,optional"` } type GetMessageLogListResponse struct { @@ -777,36 +877,10 @@ type GetMessageLogListResponse struct { List []MessageLog `json:"list"` } -type GetNodeDetailRequest struct { - Id int64 `form:"id" validate:"required"` -} - -type GetNodeGroupListResponse struct { - Total int64 `json:"total"` - List []ServerGroup `json:"list"` -} - type GetNodeMultiplierResponse struct { Periods []TimePeriod `json:"periods"` } -type GetNodeServerListRequest struct { - Page int `form:"page" validate:"required"` - Size int `form:"size" validate:"required"` - Tags string `form:"tags,omitempty"` - GroupId int64 `form:"group_id,omitempty"` - Search string `form:"search,omitempty"` -} - -type GetNodeServerListResponse struct { - Total int64 `json:"total"` - List []Server `json:"list"` -} - -type GetNodeTagListResponse struct { - Tags []string `json:"tags"` -} - type GetOAuthMethodsResponse struct { Methods []UserAuthMethod `json:"methods"` } @@ -838,9 +912,14 @@ type GetPaymentMethodListResponse struct { List []PaymentMethodDetail `json:"list"` } -type GetRuleGroupResponse struct { - Total int64 `json:"total"` - List []ServerRuleGroup `json:"list"` +type GetPreSendEmailCountRequest struct { + Scope int8 `json:"scope"` + RegisterStartTime int64 `json:"register_start_time,omitempty"` + RegisterEndTime int64 `json:"register_end_time,omitempty"` +} + +type GetPreSendEmailCountResponse struct { + Count int64 `json:"count"` } type GetServerConfigRequest struct { @@ -853,6 +932,14 @@ type GetServerConfigResponse struct { Config interface{} `json:"config"` } +type GetServerProtocolsRequest struct { + Id int64 `form:"id"` +} + +type GetServerProtocolsResponse struct { + Protocols []Protocol `json:"protocols"` +} + type GetServerUserListRequest struct { ServerCommon } @@ -862,11 +949,25 @@ type GetServerUserListResponse struct { } type GetStatResponse struct { - User int64 `json:"user"` - Node int64 `json:"node"` - Country int64 `json:"country"` - Protocol []string `json:"protocol"` - OnlineDevice int64 `json:"online_device"` + User int64 `json:"user"` + Node int64 `json:"node"` + Country int64 `json:"country"` + Protocol []string `json:"protocol"` +} + +type GetSubscribeApplicationListRequest struct { + Page int `form:"page"` + Size int `form:"size"` +} + +type GetSubscribeApplicationListResponse struct { + Total int64 `json:"total"` + List []SubscribeApplication `json:"list"` +} + +type GetSubscribeClientResponse struct { + Total int64 `json:"total"` + List []SubscribeClient `json:"list"` } type GetSubscribeDetailsRequest struct { @@ -879,10 +980,10 @@ type GetSubscribeGroupListResponse struct { } type GetSubscribeListRequest struct { - Page int64 `form:"page" validate:"required"` - Size int64 `form:"size" validate:"required"` - GroupId int64 `form:"group_id,omitempty"` - Search string `form:"search,omitempty"` + Page int64 `form:"page" validate:"required"` + Size int64 `form:"size" validate:"required"` + Language string `form:"language,omitempty"` + Search string `form:"search,omitempty"` } type GetSubscribeListResponse struct { @@ -900,6 +1001,10 @@ type GetSubscribeLogResponse struct { Total int64 `json:"total"` } +type GetSubscriptionRequest struct { + Language string `form:"language"` +} + type GetSubscriptionResponse struct { List []Subscribe `json:"list"` } @@ -958,11 +1063,6 @@ type GetUserLoginLogsResponse struct { Total int64 `json:"total"` } -type GetUserOnlineTimeStatisticsResponse struct { - WeeklyStats []WeeklyStat `json:"weekly_stats"` - ConnectionRecords ConnectionRecords `json:"connection_records"` -} - type GetUserSubscribeByIdRequest struct { Id int64 `form:"id" validate:"required"` } @@ -1002,6 +1102,17 @@ type GetUserSubscribeLogsResponse struct { Total int64 `json:"total"` } +type GetUserSubscribeResetTrafficLogsRequest struct { + Page int `form:"page"` + Size int `form:"size"` + UserSubscribeId int64 `form:"user_subscribe_id"` +} + +type GetUserSubscribeResetTrafficLogsResponse struct { + List []ResetSubscribeTrafficLog `json:"list"` + Total int64 `json:"total"` +} + type GetUserSubscribeTrafficLogsRequest struct { Page int `form:"page"` Size int `form:"size"` @@ -1032,11 +1143,32 @@ type GetUserTicketListResponse struct { List []Ticket `json:"list"` } +type GiftLog struct { + Type uint16 `json:"type"` + UserId int64 `json:"user_id"` + OrderNo string `json:"order_no"` + SubscribeId int64 `json:"subscribe_id"` + Amount int64 `json:"amount"` + Balance int64 `json:"balance"` + Remark string `json:"remark,omitempty"` + Timestamp int64 `json:"timestamp"` +} + type GoogleLoginCallbackRequest struct { Code string `form:"code"` State string `form:"state"` } +type HasMigrateSeverNodeResponse struct { + HasMigrate bool `json:"has_migrate"` +} + +type HeartbeatResponse struct { + Status bool `json:"status"` + Message string `json:"message,omitempty"` + Timestamp int64 `json:"timestamp,omitempty"` +} + type Hysteria2 struct { Port int `json:"port" validate:"required"` HopPorts string `json:"hop_ports" validate:"required"` @@ -1046,10 +1178,9 @@ type Hysteria2 struct { } type InviteConfig struct { - ForcedInvite bool `json:"forced_invite"` - FirstPurchasePercentage int64 `json:"first_purchase_percentage"` - FirstYearlyPurchasePercentage int64 `json:"first_yearly_purchase_percentage"` - NonFirstPurchasePercentage int64 `json:"non_first_purchase_percentage"` + ForcedInvite bool `json:"forced_invite"` + ReferralPercentage int64 `json:"referral_percentage"` + OnlyFirstPurchase bool `json:"only_first_purchase"` } type KickOfflineRequest struct { @@ -1060,20 +1191,39 @@ type LogResponse struct { List interface{} `json:"list"` } +type LogSetting struct { + AutoClear *bool `json:"auto_clear"` + ClearDays int64 `json:"clear_days"` +} + +type LoginLog struct { + UserId int64 `json:"user_id"` + Method string `json:"method"` + LoginIP string `json:"login_ip"` + UserAgent string `json:"user_agent"` + Success bool `json:"success"` + Timestamp int64 `json:"timestamp"` +} + type LoginResponse struct { Token string `json:"token"` } type MessageLog struct { - Id int64 `json:"id"` - Type string `json:"type"` - Platform string `json:"platform"` - To string `json:"to"` - Subject string `json:"subject"` - Content string `json:"content"` - Status int `json:"status"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` + Id int64 `json:"id"` + Type uint8 `json:"type"` + Platform string `json:"platform"` + To string `json:"to"` + Subject string `json:"subject"` + Content interface{} `json:"content"` + Status uint8 `json:"status"` + CreatedAt int64 `json:"created_at"` +} + +type MigrateServerNodeResponse struct { + Succee uint64 `json:"succee"` + Fail uint64 `json:"fail"` + Message string `json:"message,omitempty"` } type MobileAuthenticateConfig struct { @@ -1082,10 +1232,50 @@ type MobileAuthenticateConfig struct { Whitelist []string `json:"whitelist"` } +type ModuleConfig struct { + Secret string `json:"secret"` // 通讯密钥 + ServiceName string `json:"service_name"` // 服务名称 + ServiceVersion string `json:"service_version"` // 服务版本 +} + +type Node struct { + Id int64 `json:"id"` + Name string `json:"name"` + Tags []string `json:"tags"` + Port uint16 `json:"port"` + Address string `json:"address"` + ServerId int64 `json:"server_id"` + Protocol string `json:"protocol"` + Enabled *bool `json:"enabled"` + Sort int `json:"sort,omitempty"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + type NodeConfig struct { - NodeSecret string `json:"node_secret"` - NodePullInterval int64 `json:"node_pull_interval"` - NodePushInterval int64 `json:"node_push_interval"` + NodeSecret string `json:"node_secret"` + NodePullInterval int64 `json:"node_pull_interval"` + NodePushInterval int64 `json:"node_push_interval"` + TrafficReportThreshold int64 `json:"traffic_report_threshold"` + IPStrategy string `json:"ip_strategy"` + DNS []NodeDNS `json:"dns"` + Block []string `json:"block"` + Outbound []NodeOutbound `json:"outbound"` +} + +type NodeDNS struct { + Proto string `json:"proto"` + Address string `json:"address"` + Domains []string `json:"domains"` +} + +type NodeOutbound struct { + Name string `json:"name"` + Protocol string `json:"protocol"` + Address string `json:"address"` + Port int64 `json:"port"` + Password string `json:"password"` + Rules []string `json:"rules"` } type NodeRelay struct { @@ -1094,18 +1284,6 @@ type NodeRelay struct { Prefix string `json:"prefix"` } -type NodeSortRequest struct { - Sort []SortItem `json:"sort"` -} - -type NodeStatus struct { - Online interface{} `json:"online"` - Cpu float64 `json:"cpu"` - Mem float64 `json:"mem"` - Disk float64 `json:"disk"` - UpdatedAt int64 `json:"updated_at"` -} - type OAthLoginRequest struct { Method string `json:"method" validate:"required"` // google, facebook, apple, telegram, github etc. Redirect string `json:"redirect"` @@ -1288,10 +1466,73 @@ type PreUnsubscribeResponse struct { DeductionAmount int64 `json:"deduction_amount"` } +type PreViewNodeMultiplierResponse struct { + CurrentTime string `json:"current_time"` + Ratio float32 `json:"ratio"` +} + +type PreviewSubscribeTemplateRequest struct { + Id int64 `form:"id"` +} + +type PreviewSubscribeTemplateResponse struct { + Template string `json:"template"` // 预览的模板内容 +} + type PrivacyPolicyConfig struct { PrivacyPolicy string `json:"privacy_policy"` } +type Protocol struct { + Type string `json:"type"` + Port uint16 `json:"port"` + Enable bool `json:"enable"` + Security string `json:"security,omitempty"` + SNI string `json:"sni,omitempty"` + AllowInsecure bool `json:"allow_insecure,omitempty"` + Fingerprint string `json:"fingerprint,omitempty"` + RealityServerAddr string `json:"reality_server_addr,omitempty"` + RealityServerPort int `json:"reality_server_port,omitempty"` + RealityPrivateKey string `json:"reality_private_key,omitempty"` + RealityPublicKey string `json:"reality_public_key,omitempty"` + RealityShortId string `json:"reality_short_id,omitempty"` + Transport string `json:"transport,omitempty"` + Host string `json:"host,omitempty"` + Path string `json:"path,omitempty"` + ServiceName string `json:"service_name,omitempty"` + Cipher string `json:"cipher,omitempty"` + ServerKey string `json:"server_key,omitempty"` + Flow string `json:"flow,omitempty"` + HopPorts string `json:"hop_ports,omitempty"` + HopInterval int `json:"hop_interval,omitempty"` + ObfsPassword string `json:"obfs_password,omitempty"` + DisableSNI bool `json:"disable_sni,omitempty"` + ReduceRtt bool `json:"reduce_rtt,omitempty"` + UDPRelayMode string `json:"udp_relay_mode,omitempty"` + CongestionController string `json:"congestion_controller,omitempty"` + Multiplex string `json:"multiplex,omitempty"` // mux, eg: off/low/medium/high + PaddingScheme string `json:"padding_scheme,omitempty"` // padding scheme + UpMbps int `json:"up_mbps,omitempty"` // upload speed limit + DownMbps int `json:"down_mbps,omitempty"` // download speed limit + Obfs string `json:"obfs,omitempty"` // obfs, 'none', 'http', 'tls' + ObfsHost string `json:"obfs_host,omitempty"` // obfs host + ObfsPath string `json:"obfs_path,omitempty"` // obfs path + XhttpMode string `json:"xhttp_mode,omitempty"` // xhttp mode + XhttpExtra string `json:"xhttp_extra,omitempty"` // xhttp extra path + Encryption string `json:"encryption,omitempty"` // encryption,'none', 'mlkem768x25519plus' + EncryptionMode string `json:"encryption_mode,omitempty"` // encryption mode,'native', 'xorpub', 'random' + EncryptionRtt string `json:"encryption_rtt,omitempty"` // encryption rtt,'0rtt', '1rtt' + EncryptionTicket string `json:"encryption_ticket,omitempty"` // encryption ticket + EncryptionServerPadding string `json:"encryption_server_padding,omitempty"` // encryption server padding + EncryptionPrivateKey string `json:"encryption_private_key,omitempty"` // encryption private key + EncryptionClientPadding string `json:"encryption_client_padding,omitempty"` // encryption client padding + EncryptionPassword string `json:"encryption_password,omitempty"` // encryption password + Ratio float64 `json:"ratio,omitempty"` // Traffic ratio, default is 1 + CertMode string `json:"cert_mode,omitempty"` // Certificate mode, `none`|`http`|`dns`|`self` + CertDNSProvider string `json:"cert_dns_provider,omitempty"` // DNS provider for certificate + CertDNSEnv string `json:"cert_dns_env,omitempty"` // Environment for DNS provider +} + type PubilcRegisterConfig struct { StopRegister bool `json:"stop_register"` EnableIpRegisterLimit bool `json:"enable_ip_register_limit"` @@ -1316,7 +1557,7 @@ type PurchaseOrderResponse struct { type QueryAnnouncementRequest struct { Page int `form:"page"` - Size int `form:"size"` + Size int `form:"size,default=15"` Pinned *bool `form:"pinned"` Popup *bool `form:"popup"` } @@ -1335,6 +1576,20 @@ type QueryDocumentListResponse struct { List []Document `json:"list"` } +type QueryIPLocationRequest struct { + IP string `form:"ip" validate:"required"` +} + +type QueryIPLocationResponse struct { + Country string `json:"country"` + Region string `json:"region,omitempty"` + City string `json:"city"` +} + +type QueryNodeTagResponse struct { + Tags []string `json:"tags"` +} + type QueryOrderDetailRequest struct { OrderNo string `form:"order_no" validate:"required"` } @@ -1371,11 +1626,64 @@ type QueryPurchaseOrderResponse struct { Token string `json:"token,omitempty"` } +type QueryQuotaTaskListRequest struct { + Page int `form:"page"` + Size int `form:"size"` + Status *uint8 `form:"status,omitempty"` +} + +type QueryQuotaTaskListResponse struct { + Total int64 `json:"total"` + List []QuotaTask `json:"list"` +} + +type QueryQuotaTaskPreCountRequest struct { + Subscribers []int64 `json:"subscribers"` + IsActive *bool `json:"is_active"` + StartTime int64 `json:"start_time"` + EndTime int64 `json:"end_time"` +} + +type QueryQuotaTaskPreCountResponse struct { + Count int64 `json:"count"` +} + +type QueryQuotaTaskStatusRequest struct { + Id int64 `json:"id"` +} + +type QueryQuotaTaskStatusResponse struct { + Status uint8 `json:"status"` + Current int64 `json:"current"` + Total int64 `json:"total"` + Errors string `json:"errors"` +} + +type QueryServerConfigRequest struct { + ServerID int64 `path:"server_id"` + SecretKey string `form:"secret_key"` + Protocols []string `form:"protocols,omitempty"` +} + +type QueryServerConfigResponse struct { + TrafficReportThreshold int64 `json:"traffic_report_threshold"` + IPStrategy string `json:"ip_strategy"` + DNS []NodeDNS `json:"dns"` + Block []string `json:"block"` + Outbound []NodeOutbound `json:"outbound"` + Protocols []Protocol `json:"protocols"` + Total int64 `json:"total"` +} + type QuerySubscribeGroupListResponse struct { List []SubscribeGroup `json:"list"` Total int64 `json:"total"` } +type QuerySubscribeListRequest struct { + Language string `form:"language"` +} + type QuerySubscribeListResponse struct { List []Subscribe `json:"list"` Total int64 `json:"total"` @@ -1397,8 +1705,8 @@ type QueryUserAffiliateListResponse struct { } type QueryUserBalanceLogListResponse struct { - List []UserBalanceLog `json:"list"` - Total int64 `json:"total"` + List []BalanceLog `json:"list"` + Total int64 `json:"total"` } type QueryUserCommissionLogListRequest struct { @@ -1416,8 +1724,37 @@ type QueryUserSubscribeListResponse struct { Total int64 `json:"total"` } -type QueryUserSubscribeResp struct { - Data []UserSubscribeData `json:"data"` +type QueryUserSubscribeNodeListResponse struct { + List []UserSubscribeInfo `json:"list"` +} + +type QueryWithdrawalLogListRequest struct { + Page int `form:"page"` + Size int `form:"size"` +} + +type QueryWithdrawalLogListResponse struct { + List []WithdrawalLog `json:"list"` + Total int64 `json:"total"` +} + +type QuotaTask struct { + Id int64 `json:"id"` + Subscribers []int64 `json:"subscribers"` + IsActive *bool `json:"is_active"` + StartTime int64 `json:"start_time"` + EndTime int64 `json:"end_time"` + ResetTraffic bool `json:"reset_traffic"` + Days uint64 `json:"days"` + GiftType uint8 `json:"gift_type"` + GiftValue uint64 `json:"gift_value"` + Objects []int64 `json:"objects"` // UserSubscribe IDs + Status uint8 `json:"status"` + Total int64 `json:"total"` + Current int64 `json:"current"` + Errors string `json:"errors"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` } type RechargeOrderRequest struct { @@ -1440,6 +1777,15 @@ type RegisterConfig struct { IpRegisterLimitDuration int64 `json:"ip_register_limit_duration"` } +type RegisterLog struct { + UserId int64 `json:"user_id"` + AuthMethod string `json:"auth_method"` + Identifier string `json:"identifier"` + RegisterIP string `json:"register_ip"` + UserAgent string `json:"user_agent"` + Timestamp int64 `json:"timestamp"` +} + type RenewalOrderRequest struct { UserSubscribeID int64 `json:"user_subscribe_id"` Quantity int64 `json:"quantity"` @@ -1451,13 +1797,39 @@ type RenewalOrderResponse struct { OrderNo string `json:"order_no"` } +type ResetAllSubscribeTokenResponse struct { + Success bool `json:"success"` +} + type ResetPasswordRequest struct { - Email string `json:"email" validate:"required"` - Password string `json:"password" validate:"required"` - Code string `json:"code,optional"` - IP string `header:"X-Original-Forwarded-For"` - UserAgent string `header:"User-Agent"` - CfToken string `json:"cf_token,optional"` + Identifier string `json:"identifier"` + Email string `json:"email" validate:"required"` + Password string `json:"password" validate:"required"` + Code string `json:"code,optional"` + IP string `header:"X-Original-Forwarded-For"` + UserAgent string `header:"User-Agent"` + LoginType string `header:"Login-Type"` + CfToken string `json:"cf_token,optional"` +} + +type ResetSortRequest struct { + Sort []SortItem `json:"sort"` +} + +type ResetSubscribeLog struct { + Type uint16 `json:"type"` + UserId int64 `json:"user_id"` + UserSubscribeId int64 `json:"user_subscribe_id"` + OrderNo string `json:"order_no,omitempty"` + Timestamp int64 `json:"timestamp"` +} + +type ResetSubscribeTrafficLog struct { + Id int64 `json:"id"` + Type uint16 `json:"type"` + UserSubscribeId int64 `json:"user_subscribe_id"` + OrderNo string `json:"order_no,omitempty"` + Timestamp int64 `json:"timestamp"` } type ResetTrafficOrderRequest struct { @@ -1507,24 +1879,17 @@ type SendSmsCodeRequest struct { } type Server struct { - Id int64 `json:"id"` - Tags []string `json:"tags"` - Country string `json:"country"` - City string `json:"city"` - Name string `json:"name"` - ServerAddr string `json:"server_addr"` - RelayMode string `json:"relay_mode"` - RelayNode []NodeRelay `json:"relay_node"` - SpeedLimit int `json:"speed_limit"` - TrafficRatio float32 `json:"traffic_ratio"` - GroupId int64 `json:"group_id"` - Protocol string `json:"protocol"` - Config interface{} `json:"config"` - Enable *bool `json:"enable"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` - Status *NodeStatus `json:"status"` - Sort int64 `json:"sort"` + Id int64 `json:"id"` + Name string `json:"name"` + Country string `json:"country"` + City string `json:"city"` + Address string `json:"address"` + Sort int `json:"sort"` + Protocols []Protocol `json:"protocols"` + LastReportedAt int64 `json:"last_reported_at"` + Status ServerStatus `json:"status"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` } type ServerBasic struct { @@ -1546,6 +1911,20 @@ type ServerGroup struct { UpdatedAt int64 `json:"updated_at"` } +type ServerOnlineIP struct { + IP string `json:"ip"` + Protocol string `json:"protocol"` +} + +type ServerOnlineUser struct { + IP []ServerOnlineIP `json:"ip"` + UserId int64 `json:"user_id"` + Subscribe string `json:"subscribe"` + SubscribeId int64 `json:"subscribe_id"` + Traffic int64 `json:"traffic"` + ExpiredAt int64 `json:"expired_at"` +} + type ServerPushStatusRequest struct { ServerCommon Cpu float64 `json:"cpu"` @@ -1572,8 +1951,17 @@ type ServerRuleGroup struct { UpdatedAt int64 `json:"updated_at"` } +type ServerStatus struct { + Cpu float64 `json:"cpu"` + Mem float64 `json:"mem"` + Disk float64 `json:"disk"` + Protocol string `json:"protocol"` + Online []ServerOnlineUser `json:"online"` + Status string `json:"status"` +} + type ServerTotalDataResponse struct { - OnlineUserIPs int64 `json:"online_user_ips"` + OnlineUsers int64 `json:"online_users"` OnlineServers int64 `json:"online_servers"` OfflineServers int64 `json:"offline_servers"` TodayUpload int64 `json:"today_upload"` @@ -1594,6 +1982,15 @@ type ServerTrafficData struct { Download int64 `json:"download"` } +type ServerTrafficLog struct { + ServerId int64 `json:"server_id"` // Server ID + Upload int64 `json:"upload"` // Upload traffic in bytes + Download int64 `json:"download"` // Download traffic in bytes + Total int64 `json:"total"` // Total traffic in bytes (Upload + Download) + Date string `json:"date"` // Date in YYYY-MM-DD format + Details bool `json:"details"` // Whether to show detailed traffic +} + type ServerUser struct { Id int64 `json:"id"` UUID string `json:"uuid"` @@ -1637,6 +2034,10 @@ type SortItem struct { Sort int64 `json:"sort" validate:"required"` } +type StopBatchSendEmailTaskRequest struct { + Id int64 `json:"id"` +} + type StripePayment struct { Method string `json:"method"` ClientSecret string `json:"client_secret"` @@ -1646,6 +2047,7 @@ type StripePayment struct { type Subscribe struct { Id int64 `json:"id"` Name string `json:"name"` + Language string `json:"language"` Description string `json:"description"` UnitPrice int64 `json:"unit_price"` UnitTime string `json:"unit_time"` @@ -1656,10 +2058,8 @@ type Subscribe struct { SpeedLimit int64 `json:"speed_limit"` DeviceLimit int64 `json:"device_limit"` Quota int64 `json:"quota"` - GroupId int64 `json:"group_id"` - ServerGroup []int64 `json:"server_group"` - Server []int64 `json:"server"` - ServerCount int64 `json:"server_count"` + Nodes []int64 `json:"nodes"` + NodeTags []string `json:"node_tags"` Show bool `json:"show"` Sell bool `json:"sell"` Sort int64 `json:"sort"` @@ -1671,11 +2071,38 @@ type Subscribe struct { UpdatedAt int64 `json:"updated_at"` } +type SubscribeApplication struct { + Id int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Icon string `json:"icon,omitempty"` + Scheme string `json:"scheme,omitempty"` + UserAgent string `json:"user_agent"` + IsDefault bool `json:"is_default"` + SubscribeTemplate string `json:"template"` + OutputFormat string `json:"output_format"` + DownloadLink DownloadLink `json:"download_link,omitempty"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + +type SubscribeClient struct { + Id int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Icon string `json:"icon,omitempty"` + Scheme string `json:"scheme,omitempty"` + IsDefault bool `json:"is_default"` + DownloadLink DownloadLink `json:"download_link,omitempty"` +} + type SubscribeConfig struct { SingleModel bool `json:"single_model"` SubscribePath string `json:"subscribe_path"` SubscribeDomain string `json:"subscribe_domain"` PanDomain bool `json:"pan_domain"` + UserAgentLimit bool `json:"user_agent_limit"` + UserAgentList string `json:"user_agent_list"` } type SubscribeDiscount struct { @@ -1696,6 +2123,15 @@ type SubscribeItem struct { Sold int64 `json:"sold"` } +type SubscribeLog struct { + UserId int64 `json:"user_id"` + Token string `json:"token"` + UserAgent string `json:"user_agent"` + ClientIP string `json:"client_ip"` + UserSubscribeId int64 `json:"user_subscribe_id"` + Timestamp int64 `json:"timestamp"` +} + type SubscribeSortRequest struct { Sort []SortItem `json:"sort"` } @@ -1721,30 +2157,39 @@ type TelephoneCheckUserResponse struct { } type TelephoneLoginRequest struct { + Identifier string `json:"identifier"` Telephone string `json:"telephone" validate:"required"` TelephoneCode string `json:"telephone_code"` TelephoneAreaCode string `json:"telephone_area_code" validate:"required"` Password string `json:"password"` IP string `header:"X-Original-Forwarded-For"` + UserAgent string `header:"User-Agent"` + LoginType string `header:"Login-Type"` CfToken string `json:"cf_token,optional"` } type TelephoneRegisterRequest struct { + Identifier string `json:"identifier"` Telephone string `json:"telephone" validate:"required"` TelephoneAreaCode string `json:"telephone_area_code" validate:"required"` Password string `json:"password" validate:"required"` Invite string `json:"invite,optional"` Code string `json:"code,optional"` IP string `header:"X-Original-Forwarded-For"` + UserAgent string `header:"User-Agent"` + LoginType string `header:"Login-Type,optional"` CfToken string `json:"cf_token,optional"` } type TelephoneResetPasswordRequest struct { + Identifier string `json:"identifier"` Telephone string `json:"telephone" validate:"required"` TelephoneAreaCode string `json:"telephone_area_code" validate:"required"` Password string `json:"password" validate:"required"` Code string `json:"code,optional"` IP string `header:"X-Original-Forwarded-For"` + UserAgent string `header:"User-Agent"` + LoginType string `header:"Login-Type,optional"` CfToken string `json:"cf_token,optional"` } @@ -1778,6 +2223,11 @@ type TimePeriod struct { Multiplier float32 `json:"multiplier"` } +type ToggleNodeStatusRequest struct { + Id int64 `json:"id"` + Enable *bool `json:"enable"` +} + type TosConfig struct { TosContent string `json:"tos_content"` } @@ -1792,6 +2242,16 @@ type TrafficLog struct { Timestamp int64 `json:"timestamp"` } +type TrafficLogDetails struct { + Id int64 `json:"id"` + ServerId int64 `json:"server_id"` + UserId int64 `json:"user_id"` + SubscribeId int64 `json:"subscribe_id"` + Download int64 `json:"download"` + Upload int64 `json:"upload"` + Timestamp int64 `json:"timestamp"` +} + type TransportConfig struct { Path string `json:"path"` Host string `json:"host"` @@ -1824,6 +2284,10 @@ type Tuic struct { SecurityConfig SecurityConfig `json:"security_config"` } +type UnbindDeviceRequest struct { + Id int64 `json:"id" validate:"required"` +} + type UnbindOAuthRequest struct { Method string `json:"method"` } @@ -1858,25 +2322,6 @@ type UpdateAnnouncementRequest struct { Popup *bool `json:"popup"` } -type UpdateApplicationRequest struct { - Id int64 `json:"id" validate:"required"` - Icon string `json:"icon"` - Name string `json:"name"` - Description string `json:"description"` - SubscribeType string `json:"subscribe_type"` - Platform ApplicationPlatform `json:"platform"` -} - -type UpdateApplicationVersionRequest struct { - Id int64 `json:"id" validate:"required"` - Url string `json:"url"` - Version string `json:"version" validate:"required"` - Description string `json:"description"` - Platform string `json:"platform" validate:"required,oneof=windows mac linux android ios harmony"` - IsDefault bool `json:"is_default"` - ApplicationId int64 `json:"application_id" validate:"required"` -} - type UpdateAuthMethodConfigRequest struct { Id int64 `json:"id"` Method string `json:"method"` @@ -1917,28 +2362,15 @@ type UpdateDocumentRequest struct { Show *bool `json:"show"` } -type UpdateNodeGroupRequest struct { - Id int64 `json:"id" validate:"required"` - Name string `json:"name" validate:"required"` - Description string `json:"description"` -} - type UpdateNodeRequest struct { - Id int64 `json:"id" validate:"required"` - Tags []string `json:"tags"` - Country string `json:"country"` - City string `json:"city"` - Name string `json:"name" validate:"required"` - ServerAddr string `json:"server_addr" validate:"required"` - RelayMode string `json:"relay_mode"` - RelayNode []NodeRelay `json:"relay_node"` - SpeedLimit int `json:"speed_limit"` - TrafficRatio float32 `json:"traffic_ratio"` - GroupId int64 `json:"group_id"` - Protocol string `json:"protocol" validate:"required"` - Config interface{} `json:"config" validate:"required"` - Enable *bool `json:"enable"` - Sort int64 `json:"sort"` + Id int64 `json:"id"` + Name string `json:"name"` + Tags []string `json:"tags,omitempty"` + Port uint16 `json:"port"` + Address string `json:"address"` + ServerId int64 `json:"server_id"` + Protocol string `json:"protocol"` + Enabled *bool `json:"enabled"` } type UpdateOrderStatusRequest struct { @@ -1948,11 +2380,6 @@ type UpdateOrderStatusRequest struct { TradeNo string `json:"trade_no,omitempty"` } -type UpdatePasswordRequeset struct { - Password string `json:"password"` - NewPassword string `json:"new_password"` -} - type UpdatePaymentMethodRequest struct { Id int64 `json:"id" validate:"required"` Name string `json:"name" validate:"required"` @@ -1967,15 +2394,27 @@ type UpdatePaymentMethodRequest struct { Enable *bool `json:"enable" validate:"required"` } -type UpdateRuleGroupRequest struct { - Id int64 `json:"id" validate:"required"` - Icon string `json:"icon"` - Type string `json:"type"` - Name string `json:"name" validate:"required"` - Tags []string `json:"tags"` - Rules string `json:"rules"` - Default bool `json:"default"` - Enable bool `json:"enable"` +type UpdateServerRequest struct { + Id int64 `json:"id"` + Name string `json:"name"` + Country string `json:"country,omitempty"` + City string `json:"city,omitempty"` + Address string `json:"address"` + Sort int `json:"sort,omitempty"` + Protocols []Protocol `json:"protocols"` +} + +type UpdateSubscribeApplicationRequest struct { + Id int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Icon string `json:"icon,omitempty"` + Scheme string `json:"scheme,omitempty"` + UserAgent string `json:"user_agent"` + IsDefault bool `json:"is_default"` + SubscribeTemplate string `json:"template"` + OutputFormat string `json:"output_format"` + DownloadLink DownloadLink `json:"download_link,omitempty"` } type UpdateSubscribeGroupRequest struct { @@ -1987,6 +2426,7 @@ type UpdateSubscribeGroupRequest struct { type UpdateSubscribeRequest struct { Id int64 `json:"id" validate:"required"` Name string `json:"name" validate:"required"` + Language string `json:"language"` Description string `json:"description"` UnitPrice int64 `json:"unit_price"` UnitTime string `json:"unit_time"` @@ -1997,9 +2437,8 @@ type UpdateSubscribeRequest struct { SpeedLimit int64 `json:"speed_limit"` DeviceLimit int64 `json:"device_limit"` Quota int64 `json:"quota"` - GroupId int64 `json:"group_id"` - ServerGroup []int64 `json:"server_group"` - Server []int64 `json:"server"` + Nodes []int64 `json:"nodes"` + NodeTags []string `json:"node_tags"` Show *bool `json:"show"` Sell *bool `json:"sell"` Sort int64 `json:"sort"` @@ -2021,17 +2460,19 @@ type UpdateUserAuthMethodRequest struct { } type UpdateUserBasiceInfoRequest struct { - UserId int64 `json:"user_id" validate:"required"` - Password string `json:"password"` - Avatar string `json:"avatar"` - Balance int64 `json:"balance"` - Commission int64 `json:"commission"` - GiftAmount int64 `json:"gift_amount"` - Telegram int64 `json:"telegram"` - ReferCode string `json:"refer_code"` - RefererId int64 `json:"referer_id"` - Enable bool `json:"enable"` - IsAdmin bool `json:"is_admin"` + UserId int64 `json:"user_id" validate:"required"` + Password string `json:"password"` + Avatar string `json:"avatar"` + Balance int64 `json:"balance"` + Commission int64 `json:"commission"` + ReferralPercentage uint8 `json:"referral_percentage"` + OnlyFirstPurchase bool `json:"only_first_purchase"` + GiftAmount int64 `json:"gift_amount"` + Telegram int64 `json:"telegram"` + ReferCode string `json:"refer_code"` + RefererId int64 `json:"referer_id"` + Enable bool `json:"enable"` + IsAdmin bool `json:"is_admin"` } type UpdateUserNotifyRequest struct { @@ -2053,6 +2494,15 @@ type UpdateUserPasswordRequest struct { Password string `json:"password" validate:"required"` } +type UpdateUserRulesRequest struct { + Rules []string `json:"rules" validate:"required"` +} + +type UpdateUserSubscribeNoteRequest struct { + UserSubscribeId int64 `json:"user_subscribe_id" validate:"required"` + Note string `json:"note" validate:"max=500"` +} + type UpdateUserSubscribeRequest struct { UserSubscribeId int64 `json:"user_subscribe_id"` SubscribeId int64 `json:"subscribe_id"` @@ -2072,6 +2522,8 @@ type User struct { Avatar string `json:"avatar"` Balance int64 `json:"balance"` Commission int64 `json:"commission"` + ReferralPercentage uint8 `json:"referral_percentage"` + OnlyFirstPurchase bool `json:"only_first_purchase"` GiftAmount int64 `json:"gift_amount"` Telegram int64 `json:"telegram"` ReferCode string `json:"refer_code"` @@ -2084,6 +2536,7 @@ type User struct { EnableTradeNotify bool `json:"enable_trade_notify"` AuthMethods []UserAuthMethod `json:"auth_methods"` UserDevices []UserDevice `json:"user_devices"` + Rules []string `json:"rules"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` DeletedAt int64 `json:"deleted_at,omitempty"` @@ -2103,16 +2556,6 @@ type UserAuthMethod struct { Verified bool `json:"verified"` } -type UserBalanceLog struct { - Id int64 `json:"id"` - UserId int64 `json:"user_id"` - Amount int64 `json:"amount"` - Type uint8 `json:"type"` - OrderId int64 `json:"order_id"` - Balance int64 `json:"balance"` - CreatedAt int64 `json:"created_at"` -} - type UserDevice struct { Id int64 `json:"id"` Ip string `json:"ip"` @@ -2124,44 +2567,35 @@ type UserDevice struct { UpdatedAt int64 `json:"updated_at"` } -type UserInfoResponse struct { - Id int64 `json:"id"` - Balance int64 `json:"balance"` - Email string `json:"email"` - RefererId int64 `json:"referer_id"` - ReferCode string `json:"refer_code"` - Avatar string `json:"avatar"` - AreaCode string `json:"area_code"` - Telephone string `json:"telephone"` - Devices []UserDevice `json:"devices"` - AuthMethods []UserAuthMethod `json:"auth_methods"` -} - type UserLoginLog struct { Id int64 `json:"id"` UserId int64 `json:"user_id"` LoginIP string `json:"login_ip"` UserAgent string `json:"user_agent"` Success bool `json:"success"` - CreatedAt int64 `json:"created_at"` + Timestamp int64 `json:"timestamp"` } type UserLoginRequest struct { - Email string `json:"email" validate:"required"` - Password string `json:"password" validate:"required"` - IP string `header:"X-Original-Forwarded-For"` - UserAgent string `header:"User-Agent"` - CfToken string `json:"cf_token,optional"` + Identifier string `json:"identifier"` + Email string `json:"email" validate:"required"` + Password string `json:"password" validate:"required"` + IP string `header:"X-Original-Forwarded-For"` + UserAgent string `header:"User-Agent"` + LoginType string `header:"Login-Type"` + CfToken string `json:"cf_token,optional"` } type UserRegisterRequest struct { - Email string `json:"email" validate:"required"` - Password string `json:"password" validate:"required"` - Invite string `json:"invite,optional"` - Code string `json:"code,optional"` - IP string `header:"X-Original-Forwarded-For"` - UserAgent string `header:"User-Agent"` - CfToken string `json:"cf_token,optional"` + Identifier string `json:"identifier"` + Email string `json:"email" validate:"required"` + Password string `json:"password" validate:"required"` + Invite string `json:"invite,optional"` + Code string `json:"code,optional"` + IP string `header:"X-Original-Forwarded-For"` + UserAgent string `header:"User-Agent"` + LoginType string `header:"Login-Type"` + CfToken string `json:"cf_token,optional"` } type UserStatistics struct { @@ -2193,15 +2627,11 @@ type UserSubscribe struct { Upload int64 `json:"upload"` Token string `json:"token"` Status uint8 `json:"status"` + Short string `json:"short"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` } -type UserSubscribeData struct { - SubscribeId int64 `json:"subscribe_id"` - UserSubscribeId int64 `json:"user_subscribe_id"` -} - type UserSubscribeDetail struct { Id int64 `json:"id"` UserId int64 `json:"user_id"` @@ -2221,6 +2651,26 @@ type UserSubscribeDetail struct { UpdatedAt int64 `json:"updated_at"` } +type UserSubscribeInfo struct { + Id int64 `json:"id"` + UserId int64 `json:"user_id"` + OrderId int64 `json:"order_id"` + SubscribeId int64 `json:"subscribe_id"` + StartTime int64 `json:"start_time"` + ExpireTime int64 `json:"expire_time"` + FinishedAt int64 `json:"finished_at"` + ResetTime int64 `json:"reset_time"` + Traffic int64 `json:"traffic"` + Download int64 `json:"download"` + Upload int64 `json:"upload"` + Token string `json:"token"` + Status uint8 `json:"status"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + IsTryOut bool `json:"is_try_out"` + Nodes []*UserSubscribeNodeInfo `json:"nodes"` +} + type UserSubscribeLog struct { Id int64 `json:"id"` UserId int64 `json:"user_id"` @@ -2228,15 +2678,30 @@ type UserSubscribeLog struct { Token string `json:"token"` IP string `json:"ip"` UserAgent string `json:"user_agent"` - CreatedAt int64 `json:"created_at"` + Timestamp int64 `json:"timestamp"` } -type UserSubscribeResetPeriodRequest struct { - UserSubscribeId int64 `json:"user_subscribe_id"` +type UserSubscribeNodeInfo struct { + Id int64 `json:"id"` + Name string `json:"name"` + Uuid string `json:"uuid"` + Protocol string `json:"protocol"` + Port uint16 `json:"port"` + Address string `json:"address"` + Tags []string `json:"tags"` + Country string `json:"country"` + City string `json:"city"` + CreatedAt int64 `json:"created_at"` } -type UserSubscribeResetPeriodResponse struct { - Status bool `json:"status"` +type UserSubscribeTrafficLog struct { + SubscribeId int64 `json:"subscribe_id"` // Subscribe ID + UserId int64 `json:"user_id"` // User ID + Upload int64 `json:"upload"` // Upload traffic in bytes + Download int64 `json:"download"` // Download traffic in bytes + Total int64 `json:"total"` // Total traffic in bytes (Upload + Download) + Date string `json:"date"` // Date in YYYY-MM-DD format + Details bool `json:"details"` // Whether to show detailed traffic } type UserTraffic struct { @@ -2277,6 +2742,10 @@ type VerifyEmailRequest struct { Code string `json:"code" validate:"required"` } +type VersionResponse struct { + Version string `json:"version"` +} + type Vless struct { Port int `json:"port" validate:"required"` Flow string `json:"flow" validate:"required"` @@ -2313,8 +2782,13 @@ type VmessProtocol struct { Transport string `json:"transport"` } -type WeeklyStat struct { - Day int `json:"day"` - DayName string `json:"day_name"` - Hours float64 `json:"hours"` +type WithdrawalLog struct { + Id int64 `json:"id"` + UserId int64 `json:"user_id"` + Amount int64 `json:"amount"` + Content string `json:"content"` + Status uint8 `json:"status"` + Reason string `json:"reason,omitempty"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` } diff --git a/pkg/adapter/adapter.go b/pkg/adapter/adapter.go deleted file mode 100644 index be121b9..0000000 --- a/pkg/adapter/adapter.go +++ /dev/null @@ -1,102 +0,0 @@ -package adapter - -import ( - "embed" - - "github.com/perfect-panel/server/internal/model/server" - "github.com/perfect-panel/server/pkg/adapter/clash" - "github.com/perfect-panel/server/pkg/adapter/general" - "github.com/perfect-panel/server/pkg/adapter/loon" - "github.com/perfect-panel/server/pkg/adapter/proxy" - "github.com/perfect-panel/server/pkg/adapter/quantumultx" - "github.com/perfect-panel/server/pkg/adapter/shadowrocket" - "github.com/perfect-panel/server/pkg/adapter/singbox" - "github.com/perfect-panel/server/pkg/adapter/surfboard" - "github.com/perfect-panel/server/pkg/adapter/surge" - "github.com/perfect-panel/server/pkg/adapter/v2rayn" -) - -//go:embed template/* -var TemplateFS embed.FS - -var ( - AutoSelect = "Auto - UrlTest" -) - -type Config struct { - Nodes []*server.Server - Rules []*server.RuleGroup - Tags map[string][]*server.Server -} - -type Adapter struct { - proxy.Adapter -} - -func NewAdapter(cfg *Config) *Adapter { - // 转换服务器列表 - proxies, nodes, tags := adapterProxies(cfg.Nodes) - // 转换规则组 - g, r, d := adapterRules(cfg.Rules) - if d == "" { - d = AutoSelect - } - // 生成默认代理组 - proxyGroup := append(generateDefaultGroup(), g...) - // 合并代理组 - proxyGroup = SortGroups(proxyGroup, nodes, tags, d) - return &Adapter{ - Adapter: proxy.Adapter{ - Proxies: proxies, - Group: proxyGroup, - Rules: r, - Nodes: nodes, - Default: d, - TemplateFS: &TemplateFS, - }, - } -} - -// BuildClash generates a Clash configuration for the given UUID. -func (m *Adapter) BuildClash(uuid string) ([]byte, error) { - client := clash.NewClash(m.Adapter) - return client.Build(uuid) -} - -// BuildGeneral generates a general configuration for the given UUID. -func (m *Adapter) BuildGeneral(uuid string) []byte { - return general.GenerateBase64General(m.Proxies, uuid) -} - -// BuildLoon generates a Loon configuration for the given UUID. -func (m *Adapter) BuildLoon(uuid string) []byte { - return loon.BuildLoon(m.Proxies, uuid) -} - -// BuildQuantumultX generates a Quantumult X configuration for the given UUID. -func (m *Adapter) BuildQuantumultX(uuid string) string { - return quantumultx.BuildQuantumultX(m.Proxies, uuid) -} - -// BuildSingbox generates a Singbox configuration for the given UUID. -func (m *Adapter) BuildSingbox(uuid string) ([]byte, error) { - return singbox.BuildSingbox(m.Adapter, uuid) -} -func (m *Adapter) BuildShadowrocket(uuid string, userInfo shadowrocket.UserInfo) []byte { - return shadowrocket.BuildShadowrocket(m.Proxies, uuid, userInfo) -} - -// BuildSurfboard generates a Surfboard configuration for the given site name and user info. -func (m *Adapter) BuildSurfboard(siteName string, user surfboard.UserInfo) []byte { - return surfboard.BuildSurfboard(m.Adapter, siteName, user) -} - -// BuildV2rayN generates a V2rayN configuration for the given UUID. -func (m *Adapter) BuildV2rayN(uuid string) []byte { - return v2rayn.NewV2rayN(m.Adapter).Build(uuid) -} - -// BuildSurge generates a Surge configuration for the given UUID and site name. -func (m *Adapter) BuildSurge(siteName string, user surge.UserInfo) []byte { - return surge.NewSurge(m.Adapter).Build(siteName, user) -} diff --git a/pkg/adapter/adapter_test.go b/pkg/adapter/adapter_test.go deleted file mode 100644 index 73c86f6..0000000 --- a/pkg/adapter/adapter_test.go +++ /dev/null @@ -1,138 +0,0 @@ -package adapter - -import ( - "encoding/json" - "fmt" - "testing" - "time" - - "github.com/perfect-panel/server/internal/model/server" - "github.com/perfect-panel/server/pkg/adapter/surfboard" -) - -func createTestServer() []*server.Server { - c := server.Shadowsocks{ - Method: "aes-256-gcm", - Port: 10301, - ServerKey: "", - } - data, _ := json.Marshal(c) - - relays := creatRelayNode() - relay, _ := json.Marshal(relays) - enable := true - // 创建一个测试用的服务器列表 - return []*server.Server{ - { - Id: 1, - Name: "Test Server 1", - Tags: "", - Country: "CN", - City: "", - Latitude: "", - Longitude: "", - ServerAddr: "test1.example.com", - RelayMode: "random", - RelayNode: string(relay), - SpeedLimit: 0, - TrafficRatio: 0, - GroupId: 0, - Protocol: "shadowsocks", - Config: string(data), - Enable: &enable, - Sort: 0, - }, - } -} -func creatRelayNode() []*server.NodeRelay { - var nodes []*server.NodeRelay - for i := 0; i < 10; i++ { - port := 10301 + i - c := server.NodeRelay{ - Host: fmt.Sprintf("192.168.1.%d", i), - Port: port, - Prefix: fmt.Sprintf("relay-%d", i), - } - nodes = append(nodes, &c) - } - return nodes -} - -func TestNewAdapter(t *testing.T) { - nodes := createTestServer() - - rules := []*server.RuleGroup{ - { - Name: "Test Rule Group 1", - Tags: "", - Rules: "DOMAIN-SUFFIX,example.com,Test Rule Group 1", - }, - } - - adapter := NewAdapter(nodes, rules) - bytes, err := adapter.BuildClash("some-uuid") - if err != nil { - t.Errorf("Failed to build adapter: %v", err) - return - } - t.Logf("Adapter built successfully: %s", string(bytes)) -} - -func TestAdapter_BuildSingbox(t *testing.T) { - nodes := createTestServer() - - rules := []*server.RuleGroup{ - { - Name: "Test Rule Group 1", - Tags: "", - Rules: "DOMAIN-SUFFIX,example.com,Test Rule Group 1", - }, - } - - adapter := NewAdapter(nodes, rules) - bytes, err := adapter.BuildSingbox("some-uuid") - if err != nil { - t.Errorf("Failed to build adapter: %v", err) - return - } - var pretty map[string]interface{} - _ = json.Unmarshal(bytes, &pretty) - - if pretty == nil { - t.Errorf("Failed to parse Singbox config") - return - } - - prettyStr, err := json.MarshalIndent(pretty, "", " ") - if err != nil { - t.Errorf("Failed to format Singbox config: %v", err) - return - } - t.Logf("Adapter built successfully: \n %s", string(prettyStr)) -} - -func TestAdapter_BuildSurfboard(t *testing.T) { - nodes := createTestServer() - rules := []*server.RuleGroup{ - { - Name: "Test Rule Group 1", - Tags: "", - Rules: "DOMAIN-SUFFIX,example.com,Test Rule Group 1", - }, - } - adapter := NewAdapter(nodes, rules) - user := surfboard.UserInfo{ - UUID: "some-uuid", - Upload: 200, - Download: 13012, - TotalTraffic: 1024000, - ExpiredDate: time.Now().Add(24 * time.Hour), - SubscribeURL: "", - } - bytes := adapter.BuildSurfboard("test-site", user) - if bytes == nil { - t.Errorf("Failed to build adapter") - return - } - t.Logf("Adapter built successfully: %s", string(bytes)) -} diff --git a/pkg/adapter/clash/clash.go b/pkg/adapter/clash/clash.go deleted file mode 100644 index 0f9f35c..0000000 --- a/pkg/adapter/clash/clash.go +++ /dev/null @@ -1,95 +0,0 @@ -package clash - -import ( - "bytes" - "fmt" - "text/template" - - "github.com/Masterminds/sprig/v3" - "github.com/perfect-panel/server/pkg/adapter/proxy" - "github.com/perfect-panel/server/pkg/logger" - "gopkg.in/yaml.v3" -) - -type Clash struct { - proxy.Adapter -} - -func NewClash(adapter proxy.Adapter) *Clash { - return &Clash{ - Adapter: adapter, - } -} - -func (c *Clash) Build(uuid string) ([]byte, error) { - var proxies []Proxy - for _, proxied := range c.Adapter.Proxies { - p, err := c.parseProxy(proxied, uuid) - if err != nil { - logger.Errorw("Failed to parse proxy", logger.Field("error", err), logger.Field("proxy", p.Name)) - continue - } - proxies = append(proxies, *p) - } - var groups []ProxyGroup - for _, group := range c.Adapter.Group { - groups = append(groups, ProxyGroup{ - Name: group.Name, - Type: string(group.Type), - Proxies: group.Proxies, - Url: group.URL, - Interval: group.Interval, - }) - } - var rules = append(c.Rules, fmt.Sprintf("MATCH,%s", c.Default)) - - tmplBytes, err := c.TemplateFS.ReadFile("template/clash.tpl") - if err != nil { - logger.Errorw("Failed to read template file", logger.Field("error", err)) - return nil, fmt.Errorf("failed to read template file: %w", err) - } - tpl, err := template.New("clash.yaml").Funcs(sprig.FuncMap()).Funcs(template.FuncMap{ - "toYaml": func(v interface{}) string { - out, err := yaml.Marshal(v) - if err != nil { - return fmt.Sprintf("# YAML encode error: %v", err.Error()) - } - return string(out) - }, - }).Parse(string(tmplBytes)) - if err != nil { - logger.Errorw("[Clash] Failed to parse template", logger.Field("error", err)) - return nil, fmt.Errorf("failed to parse template: %w", err) - } - var buf bytes.Buffer - err = tpl.Execute(&buf, map[string]interface{}{ - "Proxies": proxies, - "ProxyGroups": groups, - "Rules": rules, - }) - if err != nil { - logger.Errorw("[Clash] Failed to execute template", logger.Field("error", err)) - return nil, fmt.Errorf("failed to execute template: %w", err) - } - - return buf.Bytes(), nil -} - -func (c *Clash) parseProxy(p proxy.Proxy, uuid string) (*Proxy, error) { - parseFuncs := map[string]func(proxy.Proxy, string) (*Proxy, error){ - "shadowsocks": parseShadowsocks, - "trojan": parseTrojan, - "vless": parseVless, - "vmess": parseVmess, - "hysteria2": parseHysteria2, - "tuic": parseTuic, - "anytls": parseAnyTLS, - } - - if parseFunc, exists := parseFuncs[p.Protocol]; exists { - return parseFunc(p, uuid) - } - - logger.Errorw("Unknown protocol", logger.Field("protocol", p.Protocol), logger.Field("server", p.Name)) - return nil, fmt.Errorf("unknown protocol: %s", p.Protocol) -} diff --git a/pkg/adapter/clash/clash_test.go b/pkg/adapter/clash/clash_test.go deleted file mode 100644 index 89c8d20..0000000 --- a/pkg/adapter/clash/clash_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package clash - -import ( - "testing" - - "github.com/perfect-panel/server/pkg/adapter/proxy" - "github.com/stretchr/testify/assert" -) - -func TestClash_Build(t *testing.T) { - adapter := proxy.Adapter{ - Proxies: []proxy.Proxy{ - { - Name: "test-proxy", - Protocol: "shadowsocks", - Server: "1.2.3.4", - Port: 8388, - Option: proxy.Shadowsocks{ - Method: "aes-256-gcm", - }, - }, - }, - Group: []proxy.Group{ - { - Name: "test-group", - Type: "select", - Proxies: []string{"test-proxy"}, - }, - }, - Rules: []string{ - "DOMAIN-SUFFIX,example.com,DIRECT", - "GEOIP,CN,DIRECT", - "MATCH,DIRECT", - }, - } - clash := NewClash(adapter) - result, err := clash.Build("test-uuid") - assert.NoError(t, err) - assert.NotNil(t, result) - -} diff --git a/pkg/adapter/clash/default.go b/pkg/adapter/clash/default.go deleted file mode 100644 index 2c2ceb0..0000000 --- a/pkg/adapter/clash/default.go +++ /dev/null @@ -1,54 +0,0 @@ -package clash - -const DefaultTemplate = ` -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: - -proxy-groups: - -rules: -` diff --git a/pkg/adapter/clash/model.go b/pkg/adapter/clash/model.go deleted file mode 100644 index 9f9925d..0000000 --- a/pkg/adapter/clash/model.go +++ /dev/null @@ -1,131 +0,0 @@ -package clash - -type RawConfig struct { - Port int `yaml:"port" json:"port"` - SocksPort int `yaml:"socks-port" json:"socks-port"` - RedirPort int `yaml:"redir-port" json:"redir-port"` - TProxyPort int `yaml:"tproxy-port" json:"tproxy-port"` - MixedPort int `yaml:"mixed-port" json:"mixed-port"` - AllowLan bool `yaml:"allow-lan" json:"allow-lan"` - Mode string `yaml:"mode" json:"mode"` - LogLevel string `yaml:"log-level" json:"log-level"` - ExternalController string `yaml:"external-controller" json:"external-controller"` - Secret string `yaml:"secret" json:"secret"` - Proxies []Proxy `yaml:"proxies" json:"proxies"` - ProxyGroups []ProxyGroup `yaml:"proxy-groups" json:"proxy-groups"` - Rules []string `yaml:"rules" json:"rule"` -} - -type Proxy struct { - // 基础数据 - Name string `yaml:"name"` - Type string `yaml:"type"` - Server string `yaml:"server"` - Port int `yaml:"port,omitempty"` - // Shadowsocks - Password string `yaml:"password,omitempty"` - Cipher string `yaml:"cipher,omitempty"` - UDP bool `yaml:"udp,omitempty"` - Plugin string `yaml:"plugin,omitempty"` - PluginOpts map[string]any `yaml:"plugin-opts,omitempty"` - UDPOverTCP bool `yaml:"udp-over-tcp,omitempty"` - UDPOverTCPVersion int `yaml:"udp-over-tcp-version,omitempty"` - ClientFingerprint string `yaml:"client-fingerprint,omitempty"` - // Vmess - UUID string `yaml:"uuid,omitempty"` - AlterID *int `yaml:"alterId,omitempty"` - Network string `yaml:"network,omitempty"` - TLS bool `yaml:"tls,omitempty"` - ALPN []string `yaml:"alpn,omitempty"` - SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"` - Fingerprint string `yaml:"fingerprint,omitempty"` - ServerName string `yaml:"servername,omitempty"` - RealityOpts RealityOptions `yaml:"reality-opts,omitempty"` - HTTPOpts HTTPOptions `yaml:"http-opts,omitempty"` - HTTP2Opts HTTP2Options `yaml:"h2-opts,omitempty"` - GrpcOpts GrpcOptions `yaml:"grpc-opts,omitempty"` - WSOpts WSOptions `yaml:"ws-opts,omitempty"` - PacketAddr bool `yaml:"packet-addr,omitempty"` - XUDP bool `yaml:"xudp,omitempty"` - PacketEncoding string `yaml:"packet-encoding,omitempty"` - GlobalPadding bool `yaml:"global-padding,omitempty"` - AuthenticatedLength bool `yaml:"authenticated-length,omitempty"` - // Vless - Flow string `yaml:"flow,omitempty"` - WSPath string `yaml:"ws-path,omitempty"` - WSHeaders map[string]string `yaml:"ws-headers,omitempty"` - // Trojan - SNI string `yaml:"sni,omitempty"` - SSOpts TrojanSSOption `yaml:"ss-opts,omitempty"` - // Hysteria2 - Ports string `yaml:"ports,omitempty"` - HopInterval int `yaml:"hop-interval,omitempty"` - Up string `yaml:"up,omitempty"` - Down string `yaml:"down,omitempty"` - Obfs string `yaml:"obfs,omitempty"` - ObfsPassword string `yaml:"obfs-password,omitempty"` - CustomCA string `yaml:"ca,omitempty"` - CustomCAString string `yaml:"ca-str,omitempty"` - CWND int `yaml:"cwnd,omitempty"` - UdpMTU int `yaml:"udp-mtu,omitempty"` - // Tuic - Token string `yaml:"token,omitempty"` - Ip string `yaml:"ip,omitempty"` - HeartbeatInterval int `yaml:"heartbeat-interval,omitempty"` - ReduceRtt bool `yaml:"reduce-rtt,omitempty"` - RequestTimeout int `yaml:"request-timeout,omitempty"` - UdpRelayMode string `yaml:"udp-relay-mode,omitempty"` - CongestionController string `yaml:"congestion-controller,omitempty"` - DisableSni bool `yaml:"disable-sni,omitempty"` - MaxUdpRelayPacketSize int `yaml:"max-udp-relay-packet-size,omitempty"` - FastOpen bool `yaml:"fast-open,omitempty"` - MaxOpenStreams int `yaml:"max-open-streams,omitempty"` - ReceiveWindowConn int `yaml:"recv-window-conn,omitempty"` - ReceiveWindow int `yaml:"recv-window,omitempty"` - DisableMTUDiscovery bool `yaml:"disable-mtu-discovery,omitempty"` - MaxDatagramFrameSize int `yaml:"max-datagram-frame-size,omitempty"` - UDPOverStream bool `yaml:"udp-over-stream,omitempty"` - UDPOverStreamVersion int `yaml:"udp-over-stream-version,omitempty"` -} -type ProxyGroup struct { - Name string `yaml:"name"` - Type string `yaml:"type"` - Proxies []string `yaml:"proxies"` - Url string `yaml:"url,omitempty"` - Interval int `yaml:"interval,omitempty"` -} - -type TrojanSSOption struct { - Enabled bool `yaml:"enabled,omitempty"` - Method string `yaml:"method,omitempty"` - Password string `yaml:"password,omitempty"` -} - -type RealityOptions struct { - PublicKey string `yaml:"public-key"` - ShortID string `yaml:"short-id"` -} - -type HTTPOptions struct { - Method string `yaml:"method,omitempty"` - Path []string `yaml:"path,omitempty"` - Headers map[string][]string `yaml:"headers,omitempty"` -} - -type HTTP2Options struct { - Host []string `yaml:"host,omitempty"` - Path string `yaml:"path,omitempty"` -} - -type GrpcOptions struct { - GrpcServiceName string `yaml:"grpc-service-name,omitempty"` -} - -type WSOptions struct { - Path string `yaml:"path,omitempty"` - Headers map[string]string `yaml:"headers,omitempty"` - MaxEarlyData int `yaml:"max-early-data,omitempty"` - EarlyDataHeaderName string `yaml:"early-data-header-name,omitempty"` - V2rayHttpUpgrade bool `yaml:"v2ray-http-upgrade,omitempty"` - V2rayHttpUpgradeFastOpen bool `yaml:"v2ray-http-upgrade-fast-open,omitempty"` -} diff --git a/pkg/adapter/clash/parse.go b/pkg/adapter/clash/parse.go deleted file mode 100644 index eb0cc95..0000000 --- a/pkg/adapter/clash/parse.go +++ /dev/null @@ -1,207 +0,0 @@ -package clash - -import ( - "fmt" - "strings" - - "github.com/perfect-panel/server/pkg/adapter/proxy" -) - -func parseShadowsocks(s proxy.Proxy, uuid string) (*Proxy, error) { - config, ok := s.Option.(proxy.Shadowsocks) - if !ok { - return nil, fmt.Errorf("invalid type for Shadowsocks") - } - p := &Proxy{ - Name: s.Name, - Type: "ss", - Server: s.Server, - Port: s.Port, - Cipher: config.Method, - Password: uuid, - UDP: true, - } - - if strings.Contains(p.Cipher, "2022") { - serverKey, userKey := proxy.GenerateShadowsocks2022Password(config, uuid) - p.Password = fmt.Sprintf("%s:%s", serverKey, userKey) - } - - return p, nil -} - -func parseTrojan(data proxy.Proxy, password string) (*Proxy, error) { - trojan, ok := data.Option.(proxy.Trojan) - if !ok { - return nil, fmt.Errorf("invalid type for Trojan") - } - p := &Proxy{ - Name: data.Name, - Type: "trojan", - Server: data.Server, - Port: data.Port, - Password: password, - SNI: trojan.SecurityConfig.SNI, - SkipCertVerify: trojan.SecurityConfig.AllowInsecure, - } - setTransportOptions(p, trojan.Transport, trojan.TransportConfig) - return p, nil -} - -func parseVless(data proxy.Proxy, uuid string) (*Proxy, error) { - vless, ok := data.Option.(proxy.Vless) - if !ok { - return nil, fmt.Errorf("invalid type for Vless") - } - p := &Proxy{ - Name: data.Name, - Type: "vless", - Server: data.Server, - Port: data.Port, - UUID: uuid, - Flow: vless.Flow, - UDP: true, - } - setSecurityOptions(p, vless.Security, vless.SecurityConfig) - clashTransport(p, vless.Transport, vless.TransportConfig) - return p, nil -} - -func parseVmess(data proxy.Proxy, uuid string) (*Proxy, error) { - vmess, ok := data.Option.(proxy.Vmess) - if !ok { - return nil, fmt.Errorf("invalid type for Vmess") - } - alterID := 0 - p := &Proxy{ - Name: data.Name, - Type: "vmess", - Server: data.Server, - Port: data.Port, - UUID: uuid, - AlterID: &alterID, - Cipher: "auto", - } - setSecurityOptions(p, vmess.Security, vmess.SecurityConfig) - clashTransport(p, vmess.Transport, vmess.TransportConfig) - return p, nil -} - -func parseHysteria2(data proxy.Proxy, uuid string) (*Proxy, error) { - hysteria2, ok := data.Option.(proxy.Hysteria2) - if !ok { - return nil, fmt.Errorf("invalid type for Hysteria2") - } - p := &Proxy{ - Name: data.Name, - Type: "hysteria2", - Server: data.Server, - Port: data.Port, - Ports: hysteria2.HopPorts, - Password: uuid, - HeartbeatInterval: hysteria2.HopInterval, - SkipCertVerify: hysteria2.SecurityConfig.AllowInsecure, - SNI: hysteria2.SecurityConfig.SNI, - } - if hysteria2.ObfsPassword != "" { - p.Obfs = "salamander" - p.ObfsPassword = hysteria2.ObfsPassword - } - - return p, nil -} - -func parseTuic(data proxy.Proxy, uuid string) (*Proxy, error) { - tuic, ok := data.Option.(proxy.Tuic) - if !ok { - return nil, fmt.Errorf("invalid type for Tuic") - } - - p := &Proxy{ - Name: data.Name, - Type: "tuic", - Server: data.Server, - Port: data.Port, - UUID: uuid, - Password: uuid, - ALPN: []string{"h3"}, - DisableSni: tuic.DisableSNI, - ReduceRtt: tuic.ReduceRtt, - CongestionController: tuic.CongestionController, - UdpRelayMode: tuic.UDPRelayMode, - SNI: tuic.SecurityConfig.SNI, - SkipCertVerify: tuic.SecurityConfig.AllowInsecure, - } - - return p, nil -} - -func parseAnyTLS(data proxy.Proxy, uuid string) (*Proxy, error) { - anyTLS, ok := data.Option.(proxy.AnyTLS) - if !ok { - return nil, fmt.Errorf("invalid type for AnyTLS") - } - - p := &Proxy{ - Name: data.Name, - Type: "anytls", - Server: data.Server, - Port: data.Port, - Password: uuid, - UDP: true, - ALPN: []string{ - "h2", - "http/1.1", - }, - } - - if anyTLS.SecurityConfig.SNI != "" { - p.SNI = anyTLS.SecurityConfig.SNI - } - if anyTLS.SecurityConfig.AllowInsecure { - p.SkipCertVerify = anyTLS.SecurityConfig.AllowInsecure - } - - return p, nil -} - -func setSecurityOptions(p *Proxy, security string, config proxy.SecurityConfig) { - switch security { - case "tls": - p.TLS = true - p.ServerName = config.SNI - p.ClientFingerprint = config.Fingerprint - p.SkipCertVerify = config.AllowInsecure - case "reality": - p.TLS = true - p.ServerName = config.SNI - p.ClientFingerprint = config.Fingerprint - p.RealityOpts = RealityOptions{ - PublicKey: config.RealityPublicKey, - ShortID: config.RealityShortId, - } - p.SkipCertVerify = config.AllowInsecure - default: - p.TLS = false - } -} - -func setTransportOptions(p *Proxy, transport string, config proxy.TransportConfig) { - switch transport { - case "websocket": - p.Network = "ws" - p.WSOpts = WSOptions{ - Path: config.Path, - Headers: map[string]string{ - "Host": config.Host, - }, - } - case "grpc": - p.Network = "grpc" - p.GrpcOpts = GrpcOptions{ - GrpcServiceName: config.ServiceName, - } - default: - p.Network = "tcp" - } -} diff --git a/pkg/adapter/clash/tool.go b/pkg/adapter/clash/tool.go deleted file mode 100644 index ee073fe..0000000 --- a/pkg/adapter/clash/tool.go +++ /dev/null @@ -1,35 +0,0 @@ -package clash - -import ( - "github.com/perfect-panel/server/pkg/adapter/proxy" -) - -func clashTransport(c *Proxy, transportType string, transportConfig proxy.TransportConfig) { - - switch transportType { - case "websocket", "httpupgrade": - if transportType == "websocket" { - c.Network = "ws" - } else { - c.Network = transportType - } - c.WSOpts = WSOptions{ - Path: transportConfig.Path, - Headers: map[string]string{}, - } - if transportConfig.Host != "" { - c.WSOpts.Headers["host"] = transportConfig.Host - } - if transportType == "httpupgrade" { - c.WSOpts.V2rayHttpUpgrade = true - } - case "grpc": - c.Network = "grpc" - c.GrpcOpts = GrpcOptions{ - GrpcServiceName: transportConfig.ServiceName, - } - case "tcp": - c.Network = "tcp" - } - -} diff --git a/pkg/adapter/general/uri.go b/pkg/adapter/general/uri.go deleted file mode 100644 index 7a4ce72..0000000 --- a/pkg/adapter/general/uri.go +++ /dev/null @@ -1,310 +0,0 @@ -package general - -import ( - "encoding/base64" - "encoding/json" - "fmt" - "net" - "net/url" - "strconv" - "strings" - - "github.com/perfect-panel/server/pkg/adapter/proxy" -) - -type v2rayShareLink struct { - Ps string `json:"ps"` - Add string `json:"add"` - Port string `json:"port"` - ID string `json:"id"` - Aid string `json:"aid"` - Net string `json:"net"` - Type string `json:"type"` - Host string `json:"host"` - SNI string `json:"sni"` - Path string `json:"path"` - TLS string `json:"tls"` - Flow string `json:"flow,omitempty"` - Alpn string `json:"alpn,omitempty"` - AllowInsecure bool `json:"allowInsecure"` - Fingerprint string `json:"fp,omitempty"` - PublicKey string `json:"pbk,omitempty"` - ShortId string `json:"sid,omitempty"` - SpiderX string `json:"spx,omitempty"` - V string `json:"v"` -} - -// GenerateBase64General will output node URLs split by '\n' and then encode into base64 -func GenerateBase64General(data []proxy.Proxy, uuid string) []byte { - var links []string - for _, v := range data { - p := buildProxy(v, uuid) - if p == "" { - continue - } - links = append(links, p) - } - var rsp []byte - rsp = base64.RawStdEncoding.AppendEncode(rsp, []byte(strings.Join(links, "\r\n"))) - return rsp -} - -func buildProxy(data proxy.Proxy, uuid string) string { - switch data.Protocol { - case "shadowsocks": - return ShadowsocksUri(data, uuid) - case "vmess": - return VmessUri(data, uuid) - case "vless": - return VlessUri(data, uuid) - case "trojan": - return TrojanUri(data, uuid) - case "hysteria2": - return Hysteria2Uri(data, uuid) - case "tuic": - return TuicUri(data, uuid) - case "anytls": - return AnyTLSUri(data, uuid) - default: - return "" - } -} - -func ShadowsocksUri(data proxy.Proxy, uuid string) string { - ss, ok := data.Option.(proxy.Shadowsocks) - if !ok { - return "" - } - - password := uuid - // SIP022 AEAD-2022 Ciphers - if strings.Contains(ss.Method, "2022") { - serverKey, userKey := proxy.GenerateShadowsocks2022Password(ss, uuid) - password = fmt.Sprintf("%s:%s", serverKey, userKey) - } - - u := &url.URL{ - Scheme: "ss", - User: url.User(strings.TrimSuffix(base64.URLEncoding.EncodeToString([]byte(ss.Method+":"+password)), "=")), - Host: net.JoinHostPort(data.Server, strconv.Itoa(data.Port)), - Fragment: data.Name, - } - return u.String() -} - -func VmessUri(data proxy.Proxy, uuid string) string { - vmess := data.Option.(proxy.Vmess) - - transport := vmess.TransportConfig - - securityConfig := vmess.SecurityConfig - - var s = v2rayShareLink{ - V: "2", - Add: data.Server, - Port: fmt.Sprint(data.Port), - ID: uuid, - Aid: "0", - Ps: data.Name, - Net: "tcp", - } - - switch vmess.Transport { - case "websocket": - s.Net = "ws" - s.Path = transport.Path - s.Host = transport.Host - case "grpc": - s.Net = "grpc" - s.Path = transport.ServiceName - case "httpupgrade": - s.Net = "http" - s.Path = transport.Path - s.Host = transport.Host - } - - if vmess.Security == "tls" { - s.TLS = "tls" - s.SNI = securityConfig.SNI - s.AllowInsecure = securityConfig.AllowInsecure - s.Fingerprint = securityConfig.Fingerprint - } - b, _ := json.Marshal(s) - return "vmess://" + strings.TrimSuffix(base64.StdEncoding.EncodeToString(b), "=") -} - -func VlessUri(data proxy.Proxy, uuid string) string { - vless := data.Option.(proxy.Vless) - transportConfig := vless.TransportConfig - securityConfig := vless.SecurityConfig - - var query = make(url.Values) - setQuery(&query, "flow", vless.Flow) - setQuery(&query, "security", vless.Security) - setQuery(&query, "encryption", "none") - - switch vless.Transport { - case "websocket": - setQuery(&query, "type", "ws") - setQuery(&query, "host", transportConfig.Host) - setQuery(&query, "path", transportConfig.Path) - - case "http2", "httpupgrade": - setQuery(&query, "type", "http") - setQuery(&query, "path", transportConfig.Path) - setQuery(&query, "host", transportConfig.Host) - case "grpc": - setQuery(&query, "type", "grpc") - setQuery(&query, "serviceName", transportConfig.ServiceName) - } - - if vless.Security == "tls" { - setQuery(&query, "sni", securityConfig.SNI) - setQuery(&query, "fp", securityConfig.Fingerprint) - } else if vless.Security == "reality" { - setQuery(&query, "pbk", securityConfig.RealityPublicKey) - setQuery(&query, "sid", securityConfig.RealityShortId) - setQuery(&query, "sni", securityConfig.SNI) - setQuery(&query, "fp", securityConfig.Fingerprint) - setQuery(&query, "servername", securityConfig.SNI) - setQuery(&query, "spx", "/") - - } - - u := url.URL{ - Scheme: "vless", - User: url.User(uuid), - Host: net.JoinHostPort(data.Server, fmt.Sprint(data.Port)), - RawQuery: query.Encode(), - Fragment: data.Name, - } - return u.String() -} - -func TrojanUri(data proxy.Proxy, uuid string) string { - trojan := data.Option.(proxy.Trojan) - transportConfig := trojan.TransportConfig - securityConfig := trojan.SecurityConfig - - var query = make(url.Values) - setQuery(&query, "security", trojan.Security) - - switch trojan.Transport { - case "websocket": - setQuery(&query, "type", "ws") - setQuery(&query, "path", transportConfig.Path) - setQuery(&query, "host", transportConfig.Host) - case "grpc": - setQuery(&query, "type", "grpc") - setQuery(&query, "serviceName", transportConfig.ServiceName) - default: - setQuery(&query, "type", "tcp") - setQuery(&query, "path", transportConfig.Path) - setQuery(&query, "host", transportConfig.Host) - } - - if securityConfig.AllowInsecure { - setQuery(&query, "allowInsecure", "1") - } - - u := &url.URL{ - Scheme: "trojan", - User: url.User(uuid), - Host: net.JoinHostPort(data.Server, strconv.Itoa(data.Port)), - RawQuery: query.Encode(), - Fragment: data.Name, - } - return u.String() -} - -func Hysteria2Uri(data proxy.Proxy, uuid string) string { - hysteria2 := data.Option.(proxy.Hysteria2) - - var query = make(url.Values) - - setQuery(&query, "sni", hysteria2.SecurityConfig.SNI) - - if hysteria2.SecurityConfig.AllowInsecure { - setQuery(&query, "insecure", "1") - } - - if hp := strings.TrimSpace(hysteria2.HopPorts); hp != "" { - setQuery(&query, "mport", hp) - } - - if hysteria2.ObfsPassword != "" { - setQuery(&query, "obfs", "salamander") - setQuery(&query, "obfs-password", hysteria2.ObfsPassword) - } - - u := &url.URL{ - Scheme: "hysteria2", - User: url.User(uuid), - Host: net.JoinHostPort(data.Server, strconv.Itoa(data.Port)), - RawQuery: query.Encode(), - Fragment: data.Name, - } - return u.String() -} - -func TuicUri(data proxy.Proxy, uuid string) string { - tuic := data.Option.(proxy.Tuic) - var query = make(url.Values) - - setQuery(&query, "congestion_control", tuic.CongestionController) - setQuery(&query, "udp_relay_mode", tuic.UDPRelayMode) - - if tuic.SecurityConfig.SNI != "" { - setQuery(&query, "sni", tuic.SecurityConfig.SNI) - } else { - setQuery(&query, "disable_sni", "1") - } - if tuic.SecurityConfig.AllowInsecure { - setQuery(&query, "allow_insecure", "1") - } - - u := &url.URL{ - Scheme: "tuic", - User: url.User(uuid + ":" + uuid), - Host: net.JoinHostPort(data.Server, strconv.Itoa(data.Port)), - RawQuery: query.Encode(), - Fragment: data.Name, - } - return u.String() -} - -func AnyTLSUri(data proxy.Proxy, uuid string) string { - anytls, ok := data.Option.(proxy.AnyTLS) - if !ok { - return "" - } - - securityConfig := anytls.SecurityConfig - var query = make(url.Values) - - // 根据AnyTLS官方URI规范实现 - // 格式: anytls://[auth@]hostname[:port]/?[key=value]&[key=value]... - - // TLS配置 - setQuery(&query, "sni", securityConfig.SNI) - - // 是否允许不安全连接 - if securityConfig.AllowInsecure { - setQuery(&query, "insecure", "1") - } - - u := url.URL{ - Scheme: "anytls", - User: url.User(uuid), - Host: net.JoinHostPort(data.Server, strconv.Itoa(anytls.Port)), - RawQuery: query.Encode(), - Fragment: data.Name, - } - return u.String() -} - -func setQuery(q *url.Values, k, v string) { - if v != "" { - q.Set(k, v) - } -} diff --git a/pkg/adapter/general/uri_test.go b/pkg/adapter/general/uri_test.go deleted file mode 100644 index d2317ae..0000000 --- a/pkg/adapter/general/uri_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package general - -import ( - "testing" - - "github.com/perfect-panel/server/pkg/adapter/proxy" -) - -func createServer() proxy.Proxy { - return proxy.Proxy{ - Name: "Meta", - Server: "127.0.0.1", - Port: 13092, - Protocol: "shadowsocks", - Option: proxy.Shadowsocks{ - Method: "aes-256-gcm", - ServerKey: "", - }, - } -} - -func TestGenerateBase64General(t *testing.T) { - s := createServer() - p := buildProxy(s, "935b33c7-e128-49f2-816b-71070469cac2") - t.Log(p) -} diff --git a/pkg/adapter/loon/build.go b/pkg/adapter/loon/build.go deleted file mode 100644 index ebbc642..0000000 --- a/pkg/adapter/loon/build.go +++ /dev/null @@ -1,61 +0,0 @@ -package loon - -import ( - "bytes" - "embed" - "strings" - "text/template" - - "github.com/perfect-panel/server/pkg/adapter/proxy" - "github.com/perfect-panel/server/pkg/logger" -) - -//go:embed *.tpl -var configFiles embed.FS - -func BuildLoon(servers []proxy.Proxy, uuid string) []byte { - uri := "" - nodes := make([]string, 0) - for _, s := range servers { - switch s.Protocol { - case "vmess": - nodes = append(nodes, s.Name) - uri += buildVMess(s, uuid) - case "shadowsocks": - nodes = append(nodes, s.Name) - uri += buildShadowsocks(s, uuid) - case "trojan": - nodes = append(nodes, s.Name) - uri += buildTrojan(s, uuid) - case "vless": - nodes = append(nodes, s.Name) - uri += buildVless(s, uuid) - case "hysteria2": - nodes = append(nodes, s.Name) - uri += buildHysteria2(s, uuid) - default: - continue - } - } - file, err := configFiles.ReadFile("default.tpl") - if err != nil { - logger.Errorf("read default surfboard config error: %v", err.Error()) - return nil - } - // replace template - tpl, err := template.New("default").Parse(string(file)) - if err != nil { - logger.Errorf("read default surfboard config error: %v", err.Error()) - return nil - } - var buf bytes.Buffer - if err = tpl.Execute(&buf, map[string]interface{}{ - "Proxies": uri, - "Nodes": strings.Join(nodes, ","), - }); err != nil { - logger.Errorf("Execute Loon template error: %v", err.Error()) - return nil - } - - return buf.Bytes() -} diff --git a/pkg/adapter/loon/default.tpl b/pkg/adapter/loon/default.tpl deleted file mode 100644 index bca1046..0000000 --- a/pkg/adapter/loon/default.tpl +++ /dev/null @@ -1,58 +0,0 @@ -[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 \ No newline at end of file diff --git a/pkg/adapter/loon/hysteria2.go b/pkg/adapter/loon/hysteria2.go deleted file mode 100644 index 9a39c96..0000000 --- a/pkg/adapter/loon/hysteria2.go +++ /dev/null @@ -1,34 +0,0 @@ -package loon - -import ( - "fmt" - "strconv" - "strings" - - "github.com/perfect-panel/server/pkg/adapter/proxy" -) - -func buildHysteria2(data proxy.Proxy, password string) string { - hysteria2 := data.Option.(proxy.Hysteria2) - - configs := []string{ - fmt.Sprintf("%s=Hysteria2", data.Name), - data.Server, - strconv.Itoa(data.Port), - password, - "udp=true", - } - if hysteria2.ObfsPassword != "" { - configs = append(configs, "obfs=salamander", fmt.Sprintf("salamander-password=%s", hysteria2.ObfsPassword)) - } - if hysteria2.SecurityConfig.SNI != "" { - configs = append(configs, fmt.Sprintf("sni=%s", hysteria2.SecurityConfig.SNI)) - if hysteria2.SecurityConfig.AllowInsecure { - configs = append(configs, "skip-cert-verify=true") - } else { - configs = append(configs, "skip-cert-verify=false") - } - } - uri := strings.Join(configs, ",") - return uri + "\r\n" -} diff --git a/pkg/adapter/loon/loon_test.go b/pkg/adapter/loon/loon_test.go deleted file mode 100644 index 25fdb2c..0000000 --- a/pkg/adapter/loon/loon_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package loon - -import ( - "testing" - - "github.com/perfect-panel/server/pkg/adapter/proxy" -) - -func createSS() proxy.Proxy { - return proxy.Proxy{ - Name: "Shadowsocks", - Server: "127.0.0.1", - Port: 10301, - Protocol: "shadowsocks", - Option: proxy.Shadowsocks{ - Method: "aes-256-gcm", - ServerKey: "", - }, - } - -} - -func TestBuildSS(t *testing.T) { - s := createSS() - - password := "f0d0237d-193a-4cf5-99dd-b02207beaea6" - uri := buildShadowsocks(s, password) - t.Log(uri) -} diff --git a/pkg/adapter/loon/shadowsocks.go b/pkg/adapter/loon/shadowsocks.go deleted file mode 100644 index 3f1d8e8..0000000 --- a/pkg/adapter/loon/shadowsocks.go +++ /dev/null @@ -1,34 +0,0 @@ -package loon - -import ( - "fmt" - "strconv" - "strings" - - "github.com/perfect-panel/server/pkg/adapter/proxy" -) - -func buildShadowsocks(data proxy.Proxy, password string) string { - shadowsocks := data.Option.(proxy.Shadowsocks) - // If the method is 2022-blake3-chacha20-poly1305, it means that the server is a relay server - if shadowsocks.Method == "2022-blake3-chacha20-poly1305" { - return "" - } - - if strings.Contains(shadowsocks.Method, "2022") { - serverKey, userKey := proxy.GenerateShadowsocks2022Password(shadowsocks, password) - password = fmt.Sprintf("%s:%s", serverKey, userKey) - } - - configs := []string{ - fmt.Sprintf("%s=Shadowsocks", data.Name), - data.Server, - strconv.Itoa(data.Port), - shadowsocks.Method, - password, - "fast-open=false", - "udp=true", - } - uri := strings.Join(configs, ",") - return uri + "\r\n" -} diff --git a/pkg/adapter/loon/trojan.go b/pkg/adapter/loon/trojan.go deleted file mode 100644 index e6cd672..0000000 --- a/pkg/adapter/loon/trojan.go +++ /dev/null @@ -1,44 +0,0 @@ -package loon - -import ( - "fmt" - "strings" - - "github.com/perfect-panel/server/pkg/adapter/proxy" -) - -func buildTrojan(data proxy.Proxy, password string) string { - trojan := data.Option.(proxy.Trojan) - - configs := []string{ - fmt.Sprintf("%s=trojan", data.Name), - data.Server, - fmt.Sprintf("%d", data.Port), - "auto", - password, - "fast-open=false", - "udp=true", - } - - if trojan.SecurityConfig.SNI != "" { - configs = append(configs, fmt.Sprintf("sni=%s", trojan.SecurityConfig.SNI)) - } - if trojan.SecurityConfig.AllowInsecure { - configs = append(configs, "skip-cert-verify=true") - } else { - configs = append(configs, "skip-cert-verify=false") - } - - if trojan.Transport == "websocket" { - configs = append(configs, "transport=ws") - if trojan.TransportConfig.Path != "" { - configs = append(configs, fmt.Sprintf("path=%s", trojan.TransportConfig.Path)) - } - if trojan.TransportConfig.Host != "" { - configs = append(configs, fmt.Sprintf("host=%s", trojan.TransportConfig.Host)) - } - } - - uri := strings.Join(configs, ",") - return uri + "\r\n" -} diff --git a/pkg/adapter/loon/vless.go b/pkg/adapter/loon/vless.go deleted file mode 100644 index 355f147..0000000 --- a/pkg/adapter/loon/vless.go +++ /dev/null @@ -1,62 +0,0 @@ -package loon - -import ( - "fmt" - "strconv" - "strings" - - "github.com/perfect-panel/server/pkg/adapter/proxy" - - "github.com/perfect-panel/server/pkg/logger" -) - -func buildVless(data proxy.Proxy, password string) string { - vless := data.Option.(proxy.Vless) - // If flow is not empty, it means that the server is a relay server - if vless.Flow != "" { - return "" - } - - configs := []string{ - fmt.Sprintf("%s=vless", data.Name), - data.Server, - strconv.Itoa(data.Port), - "auto", - password, - "fast-open=false", - "udp=true", - "alterId=0", - } - - switch vless.Transport { - case "tcp": - configs = append(configs, "transport=tcp") - case "websocket": - configs = append(configs, "transport=ws") - if vless.TransportConfig.Path != "" { - configs = append(configs, fmt.Sprintf("path=%s", vless.TransportConfig.Path)) - } - if vless.TransportConfig.Host != "" { - configs = append(configs, fmt.Sprintf("host=%s", vless.TransportConfig.Host)) - } - default: - logger.Info("Loon Unknown transport type: ", logger.Field("transport", vless.Transport)) - return "" - } - - if vless.Security == "tls" { - configs = append(configs, "over-tls=true", fmt.Sprintf("tls-name=%s", vless.SecurityConfig.SNI)) - if vless.SecurityConfig.AllowInsecure { - configs = append(configs, "skip-cert-verify=true") - } else { - configs = append(configs, "skip-cert-verify=false") - } - } else if vless.Security == "reality" { - // Loon does not support reality security - logger.Info("Loon Unknown security type: ", logger.Field("security", vless.Security)) - return "" - } - - uri := strings.Join(configs, ",") - return uri + "\r\n" -} diff --git a/pkg/adapter/loon/vmess.go b/pkg/adapter/loon/vmess.go deleted file mode 100644 index 04e7e6d..0000000 --- a/pkg/adapter/loon/vmess.go +++ /dev/null @@ -1,53 +0,0 @@ -package loon - -import ( - "fmt" - "strings" - - "github.com/perfect-panel/server/pkg/adapter/proxy" - "github.com/perfect-panel/server/pkg/logger" -) - -func buildVMess(data proxy.Proxy, password string) string { - vmess := data.Option.(proxy.Vmess) - - configs := []string{ - fmt.Sprintf("%s=vmess", data.Name), - data.Server, - fmt.Sprintf("%d", data.Port), - "auto", - password, - "fast-open=false", - "udp=true", - "alterId=0", - } - - switch vmess.Transport { - case "tcp": - configs = append(configs, "transport=tcp") - case "websocket": - configs = append(configs, "transport=ws") - if vmess.TransportConfig.Path != "" { - configs = append(configs, fmt.Sprintf("path=%s", vmess.TransportConfig.Path)) - } - if vmess.TransportConfig.Host != "" { - configs = append(configs, fmt.Sprintf("host=%s", vmess.TransportConfig.Host)) - } - default: - logger.Info("Loon Unknown transport type: ", logger.Field("transport", vmess.Transport)) - return "" - } - - if vmess.Security == "tls" { - configs = append(configs, "over-tls=true", fmt.Sprintf("tls-name=%s", vmess.SecurityConfig.SNI)) - if vmess.SecurityConfig.AllowInsecure { - configs = append(configs, "skip-cert-verify=true") - } else { - configs = append(configs, "skip-cert-verify=false") - } - - } - - uri := strings.Join(configs, ",") - return uri + "\r\n" -} diff --git a/pkg/adapter/proxy/proxy.go b/pkg/adapter/proxy/proxy.go deleted file mode 100644 index 75760ff..0000000 --- a/pkg/adapter/proxy/proxy.go +++ /dev/null @@ -1,137 +0,0 @@ -package proxy - -import "embed" - -// Adapter represents a proxy adapter -type Adapter struct { - Proxies []Proxy - Group []Group - Rules []string // rule - Nodes []string // all node - Default string // Default Node - TemplateFS *embed.FS // Template file system -} - -// Proxy represents a proxy server -type Proxy struct { - Name string // Name of the proxy - Server string // Server address of the proxy - Port int // Port of the proxy server - Protocol string // Protocol type (e.g., shadowsocks, vless, vmess, trojan, hysteria2, tuic, anytls) - Country string // Country of the proxy - Tags []string // Tags for the proxy - Option any // Additional options for the proxy configuration -} - -// Group represents a group of proxies -type Group struct { - Name string - Type GroupType - Proxies []string - URL string - Interval int - Reject bool // Reject group - Direct bool // Direct group - Tags []string // Tags for the group - Default bool // Default group -} - -type GroupType string - -const ( - GroupTypeSelect GroupType = "select" - GroupTypeURLTest GroupType = "url-test" - GroupTypeFallback GroupType = "fallback" -) - -// Shadowsocks represents a Shadowsocks proxy configuration -type Shadowsocks struct { - Port int `json:"port"` - Method string `json:"method"` - ServerKey string `json:"server_key"` -} - -// Vless represents a Vless proxy configuration -type Vless struct { - Port int `json:"port"` - Flow string `json:"flow"` - Transport string `json:"transport"` - TransportConfig TransportConfig `json:"transport_config"` - Security string `json:"security"` - SecurityConfig SecurityConfig `json:"security_config"` -} - -// Vmess represents a Vmess proxy configuration -type Vmess struct { - Port int `json:"port"` - Flow string `json:"flow"` - Transport string `json:"transport"` - TransportConfig TransportConfig `json:"transport_config"` - Security string `json:"security"` - SecurityConfig SecurityConfig `json:"security_config"` -} - -// Trojan represents a Trojan proxy configuration -type Trojan struct { - Port int `json:"port"` - Flow string `json:"flow"` - Transport string `json:"transport"` - TransportConfig TransportConfig `json:"transport_config"` - Security string `json:"security"` - SecurityConfig SecurityConfig `json:"security_config"` -} - -// Hysteria2 represents a Hysteria2 proxy configuration -type Hysteria2 struct { - Port int `json:"port"` - HopPorts string `json:"hop_ports"` - HopInterval int `json:"hop_interval"` - ObfsPassword string `json:"obfs_password"` - SecurityConfig SecurityConfig `json:"security_config"` -} - -// Tuic represents a Tuic proxy configuration -type Tuic struct { - Port int `json:"port"` - DisableSNI bool `json:"disable_sni"` - ReduceRtt bool `json:"reduce_rtt"` - UDPRelayMode string `json:"udp_relay_mode"` - CongestionController string `json:"congestion_controller"` - SecurityConfig SecurityConfig `json:"security_config"` -} - -// AnyTLS represents an AnyTLS proxy configuration -type AnyTLS struct { - Port int `json:"port"` - SecurityConfig SecurityConfig `json:"security_config"` -} - -// TransportConfig represents the transport configuration for a proxy -type TransportConfig struct { - Path string `json:"path,omitempty"` // ws/httpupgrade - Host string `json:"host,omitempty"` - ServiceName string `json:"service_name"` // grpc - DisableSNI bool `json:"disable_sni"` // Disable SNI for the transport(tuic) - ReduceRtt bool `json:"reduce_rtt"` // Reduce RTT for the transport(tuic) - UDPRelayMode string `json:"udp_relay_mode"` // UDP relay mode for the transport(tuic) - CongestionController string `json:"congestion_controller"` // Congestion controller for the transport(tuic) -} - -// SecurityConfig represents the security configuration for a proxy -type SecurityConfig struct { - SNI string `json:"sni"` - AllowInsecure bool `json:"allow_insecure"` - Fingerprint string `json:"fingerprint"` - RealityServerAddr string `json:"reality_server_addr"` - RealityServerPort int `json:"reality_server_port"` - RealityPrivateKey string `json:"reality_private_key"` - RealityPublicKey string `json:"reality_public_key"` - RealityShortId string `json:"reality_short_id"` -} - -// Relay represents a relay configuration -type Relay struct { - RelayHost string - DispatchMode string - Prefix string -} diff --git a/pkg/adapter/proxy/tool.go b/pkg/adapter/proxy/tool.go deleted file mode 100644 index 014c581..0000000 --- a/pkg/adapter/proxy/tool.go +++ /dev/null @@ -1,15 +0,0 @@ -package proxy - -import ( - "encoding/base64" - "github.com/perfect-panel/server/pkg/uuidx" -) - -func GenerateShadowsocks2022Password(ss Shadowsocks, password string) (string, string) { - if ss.Method == "2022-blake3-aes-128-gcm" { - password = uuidx.UUIDToBase64(password, 16) - } else { - password = uuidx.UUIDToBase64(password, 32) - } - return base64.StdEncoding.EncodeToString([]byte(ss.ServerKey)), password -} diff --git a/pkg/adapter/quantumultx/build.go b/pkg/adapter/quantumultx/build.go deleted file mode 100644 index 52898a9..0000000 --- a/pkg/adapter/quantumultx/build.go +++ /dev/null @@ -1,22 +0,0 @@ -package quantumultx - -import ( - "encoding/base64" - - "github.com/perfect-panel/server/pkg/adapter/proxy" -) - -func BuildQuantumultX(servers []proxy.Proxy, uuid string) string { - var uri string - for _, s := range servers { - switch s.Protocol { - case "vmess": - uri += buildVmess(s, uuid) - case "shadowsocks": - uri += buildShadowsocks(s, uuid) - case "trojan": - uri += buildTrojan(s, uuid) - } - } - return base64.StdEncoding.EncodeToString([]byte(uri)) -} diff --git a/pkg/adapter/quantumultx/quantumux_test.go b/pkg/adapter/quantumultx/quantumux_test.go deleted file mode 100644 index 35e7f88..0000000 --- a/pkg/adapter/quantumultx/quantumux_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package quantumultx - -import ( - "testing" - - "github.com/perfect-panel/server/pkg/adapter/proxy" -) - -func createVMess() proxy.Proxy { - - return proxy.Proxy{ - Name: "Vmess", - Server: "test.xxxx.com", - Port: 13002, - Protocol: "vmess", - Option: proxy.Vmess{ - Port: 13002, - Transport: "websocket", - TransportConfig: proxy.TransportConfig{ - Path: "/ws", - Host: "test.xx.com", - }, - Security: "none", - }, - } -} - -func createSS() proxy.Proxy { - return proxy.Proxy{ - Name: "Shadowsocks", - Server: "test.xxxx.com", - Port: 10301, - Protocol: "shadowsocks", - Option: proxy.Shadowsocks{ - Port: 10301, - Method: "aes-256-gcm", - ServerKey: "123456", - }, - } -} - -func createTrojan() proxy.Proxy { - - return proxy.Proxy{ - Name: "Trojan", - Server: "test.xxxx.com", - Port: 13002, - Protocol: "trojan", - Option: proxy.Trojan{ - Port: 13002, - Transport: "websocket", - TransportConfig: proxy.TransportConfig{ - Path: "/ws", - Host: "baidu.com", - }, - SecurityConfig: proxy.SecurityConfig{ - SNI: "baidu.com", - AllowInsecure: true, - }, - }, - } -} -func TestVmess(t *testing.T) { - s := createVMess() - vmess := buildVmess(s, "uuid") - t.Log(vmess) - // output: - // vmess=127.0.0.1:13002,method=chacha20-poly1305,password=uuid,fast-open=true,udp-relay=true,tag=Vmess,tls-verification=true,obfs-uri=/ws,obfs-host=baidu.com -} - -func TestShadowsocks(t *testing.T) { - s := createSS() - shadowsocks := buildShadowsocks(s, "uuid") - t.Log(shadowsocks) - // output: - // shadowsocks=127.0.0.1:10301,method=aes-256-gcm,password=uuid,fast-open=true,udp-relay=true,tag=Shadowsocks -} - -func TestTrojan(t *testing.T) { - s := createTrojan() - trojan := buildTrojan(s, "password") - t.Log(trojan) - // output: - // trojan=192.168.0.1:13002,password=password,fast-open=true,udp-relay=true,tag=Trojan,obfs=wss,obfs-uri=ws,obfs-host=baidu.com -} - -func TestBuildQuantumultX(t *testing.T) { - var servers []proxy.Proxy - uri := BuildQuantumultX(servers, "uuid") - t.Log(uri) - - // output: - // c2hhZG93c29ja3M9MTI3LjAuMC4xOjEwMzAxLG1ldGhvZD1hZXMtMjU2LWdjbSxwYXNzd29yZD11dWlkLGZhc3Qtb3Blbj10cnVlLHVkcC1yZWxheT10cnVlLHRhZz1TaGFkb3dzb2Nrcw0KdHJvamFuPTE5Mi4xNjguMC4xOjEzMDAyLHBhc3N3b3JkPXV1aWQsZmFzdC1vcGVuPXRydWUsdWRwLXJlbGF5PXRydWUsdGFnPVRyb2phbixvYmZzPXdzcyxvYmZzLXVyaT13cyxvYmZzLWhvc3Q9YmFpZHUuY29tDQp2bWVzcz0xMjcuMC4wLjE6MTMwMDIsbWV0aG9kPWNoYWNoYTIwLXBvbHkxMzA1LHBhc3N3b3JkPXV1aWQsZmFzdC1vcGVuPXRydWUsdWRwLXJlbGF5PXRydWUsdGFnPVZtZXNzLHRscy12ZXJpZmljYXRpb249dHJ1ZSxvYmZzLXVyaT0vd3Msb2Jmcy1ob3N0PWJhaWR1LmNvbQ0K -} diff --git a/pkg/adapter/quantumultx/shadowsocks.go b/pkg/adapter/quantumultx/shadowsocks.go deleted file mode 100644 index ca1f253..0000000 --- a/pkg/adapter/quantumultx/shadowsocks.go +++ /dev/null @@ -1,30 +0,0 @@ -package quantumultx - -import ( - "fmt" - "strings" - - "github.com/perfect-panel/server/pkg/adapter/proxy" -) - -func buildShadowsocks(data proxy.Proxy, uuid string) string { - ss := data.Option.(proxy.Shadowsocks) - addr := fmt.Sprintf("%s:%d", data.Server, data.Port) - - password := uuid - - if strings.Contains(ss.Method, "2022") { - serverKey, userKey := proxy.GenerateShadowsocks2022Password(ss, uuid) - password = fmt.Sprintf("%s:%s", serverKey, userKey) - } - - config := []string{ - addr, - fmt.Sprintf("method=%s", ss.Method), - fmt.Sprintf("password=%s", password), - "fast-open=true", - "udp-relay=true", - fmt.Sprintf("tag=%s", data.Name), - } - return strings.Join(config, ",") + "\r\n" -} diff --git a/pkg/adapter/quantumultx/trojan.go b/pkg/adapter/quantumultx/trojan.go deleted file mode 100644 index 7a701aa..0000000 --- a/pkg/adapter/quantumultx/trojan.go +++ /dev/null @@ -1,39 +0,0 @@ -package quantumultx - -import ( - "fmt" - "strings" - - "github.com/perfect-panel/server/pkg/adapter/proxy" -) - -// 生成 Trojan 配置 -func buildTrojan(data proxy.Proxy, password string) string { - trojan := data.Option.(proxy.Trojan) - - addr := fmt.Sprintf("trojan=%s:%d", data.Server, data.Port) - config := []string{ - addr, - fmt.Sprintf("password=%s", password), - "fast-open=true", - "udp-relay=true", - fmt.Sprintf("tag=%s", data.Name), - } - - if trojan.Transport == "websocket" { - config = append(config, "obfs=wss") - if trojan.TransportConfig.Path != "" { - config = append(config, fmt.Sprintf("obfs-uri=%s", trojan.TransportConfig.Path)) - } - if trojan.TransportConfig.Host != "" { - config = append(config, fmt.Sprintf("obfs-host=%s", trojan.TransportConfig.Host)) - } - } else { - config = append(config, "over-tls=true") - if trojan.SecurityConfig.SNI != "" { - config = append(config, fmt.Sprintf("tls-host=%s", trojan.SecurityConfig.SNI)) - } - } - - return strings.Join(config, ",") + "\r\n" -} diff --git a/pkg/adapter/quantumultx/vmess.go b/pkg/adapter/quantumultx/vmess.go deleted file mode 100644 index d448a94..0000000 --- a/pkg/adapter/quantumultx/vmess.go +++ /dev/null @@ -1,45 +0,0 @@ -package quantumultx - -import ( - "fmt" - "strings" - - "github.com/perfect-panel/server/pkg/adapter/proxy" -) - -func buildVmess(data proxy.Proxy, uuid string) string { - - vmess := data.Option.(proxy.Vmess) - addr := fmt.Sprintf("vmess=%s:%d", data.Server, data.Port) - var host string - uriConfig := []string{ - addr, - "method=chacha20-poly1305", - fmt.Sprintf("password=%s", uuid), - "fast-open=true", - "udp-relay=true", - fmt.Sprintf("tag=%s", data.Name), - } - if vmess.Security == "tls" { - if vmess.Transport == "tcp" { - uriConfig = append(uriConfig, "obfs=over-tls") - } - if vmess.SecurityConfig.AllowInsecure { - uriConfig = append(uriConfig, "tls-verification=true") - } else { - uriConfig = append(uriConfig, "tls-verification=false") - } - if vmess.SecurityConfig.SNI != "" { - host = vmess.SecurityConfig.SNI - } - } - - if vmess.Transport == "websocket" { - uriConfig = append(uriConfig, fmt.Sprintf("obfs-uri=%s", vmess.TransportConfig.Path)) - host = vmess.TransportConfig.Host - } - if host != "" { - uriConfig = append(uriConfig, fmt.Sprintf("obfs-host=%s", host)) - } - return strings.Join(uriConfig, ",") + "\r\n" -} diff --git a/pkg/adapter/shadowrocket/build.go b/pkg/adapter/shadowrocket/build.go deleted file mode 100644 index b4b61a5..0000000 --- a/pkg/adapter/shadowrocket/build.go +++ /dev/null @@ -1,48 +0,0 @@ -package shadowrocket - -import ( - "fmt" - "time" - - "github.com/perfect-panel/server/pkg/adapter/general" - - "encoding/base64" - - "github.com/perfect-panel/server/pkg/adapter/proxy" - "github.com/perfect-panel/server/pkg/traffic" -) - -type UserInfo struct { - Upload int64 - Download int64 - TotalTraffic int64 - ExpiredDate time.Time -} - -func BuildShadowrocket(servers []proxy.Proxy, uuid string, userinfo UserInfo) []byte { - upload := traffic.AutoConvert(userinfo.Upload, false) - download := traffic.AutoConvert(userinfo.Download, false) - total := traffic.AutoConvert(userinfo.TotalTraffic, false) - expiredAt := userinfo.ExpiredDate.Format("2006-01-02 15:04:05") - uri := fmt.Sprintf("STATUS=🚀↑:%s,↓:%s,TOT:%s💡Expires:%s\r\n", upload, download, total, expiredAt) - for _, s := range servers { - switch s.Protocol { - case "vmess": - uri += buildVmess(s, uuid) - case "shadowsocks": - uri += general.ShadowsocksUri(s, uuid) + "\r\n" - case "trojan": - uri += general.TrojanUri(s, uuid) + "\r\n" - case "vless": - uri += general.VlessUri(s, uuid) + "\r\n" - case "hysteria2": - uri += general.Hysteria2Uri(s, uuid) + "\r\n" - case "tuic": - uri += general.TuicUri(s, uuid) + "\r\n" - default: - continue - } - } - - return []byte(base64.StdEncoding.EncodeToString([]byte(uri))) -} diff --git a/pkg/adapter/shadowrocket/shadowrocket_test.go b/pkg/adapter/shadowrocket/shadowrocket_test.go deleted file mode 100644 index c4266ce..0000000 --- a/pkg/adapter/shadowrocket/shadowrocket_test.go +++ /dev/null @@ -1,76 +0,0 @@ -package shadowrocket - -import ( - "testing" - "time" - - "github.com/perfect-panel/server/pkg/adapter/proxy" -) - -func createVMess() proxy.Proxy { - return proxy.Proxy{ - Name: "Vmess", - Server: "test.xxxx.com", - Port: 13002, - Protocol: "vmess", - Option: proxy.Vmess{ - Port: 13002, - Transport: "websocket", - TransportConfig: proxy.TransportConfig{ - Path: "/ws", - Host: "test.xx.com", - }, - Security: "none", - }, - } -} - -func createSS() proxy.Proxy { - return proxy.Proxy{ - Name: "Shadowsocks", - Server: "test.xxxx.com", - Port: 10301, - Protocol: "shadowsocks", - Option: proxy.Shadowsocks{ - Port: 10301, - Method: "aes-256-gcm", - ServerKey: "123456", - }, - } -} - -func createTrojan() proxy.Proxy { - - return proxy.Proxy{ - Name: "Trojan", - Server: "test.xxxx.com", - Port: 13002, - Protocol: "trojan", - Option: proxy.Trojan{ - Port: 13002, - Transport: "websocket", - TransportConfig: proxy.TransportConfig{ - Path: "/ws", - Host: "baidu.com", - }, - SecurityConfig: proxy.SecurityConfig{ - SNI: "baidu.com", - AllowInsecure: true, - }, - }, - } -} -func TestBuildShadowrocket(t *testing.T) { - s := []proxy.Proxy{ - createVMess(), - createSS(), - createTrojan(), - } - uri := BuildShadowrocket(s, "uuid", UserInfo{ - Upload: 1024, - Download: 1024, - TotalTraffic: 2048, - ExpiredDate: time.Now().AddDate(0, 0, 1), - }) - t.Log(string(uri)) -} diff --git a/pkg/adapter/shadowrocket/vmess.go b/pkg/adapter/shadowrocket/vmess.go deleted file mode 100644 index 45ae526..0000000 --- a/pkg/adapter/shadowrocket/vmess.go +++ /dev/null @@ -1,57 +0,0 @@ -package shadowrocket - -import ( - "fmt" - "strings" - - "encoding/base64" - - "github.com/perfect-panel/server/pkg/adapter/proxy" -) - -func buildVmess(data proxy.Proxy, uuid string) string { - vmess := data.Option.(proxy.Vmess) - - userinfo := fmt.Sprintf("auto:%s@%s:%d", uuid, data.Server, data.Port) - // 准备 config,使用默认值 - config := map[string]interface{}{ - "tfo": 1, - "remark": data.Name, - "alterId": 0, - } - - // tls 配置 - if vmess.Security == "tls" { - config["tls"] = 1 - if vmess.SecurityConfig.AllowInsecure { - config["allowInsecure"] = 1 - } - if vmess.SecurityConfig.SNI != "" { - config["peer"] = vmess.SecurityConfig.SNI - } - } - - // transport 配置 - switch vmess.Transport { - case "websocket": - config["obfs"] = "websocket" - if vmess.TransportConfig.Path != "" { - config["path"] = vmess.TransportConfig.Path - } - if vmess.TransportConfig.Host != "" { - config["obfsParam"] = vmess.TransportConfig.Host - } - case "grpc": - config["obfs"] = "grpc" - if vmess.TransportConfig.ServiceName != "" { - config["path"] = vmess.TransportConfig.ServiceName - } - } - query := make([]string, 0) - for k, v := range config { - query = append(query, fmt.Sprintf("%s=%v", k, v)) - } - queryStr := strings.Join(query, "&") - uri := fmt.Sprintf("vmess://%s?%s\r\n", base64.StdEncoding.EncodeToString([]byte(userinfo)), queryStr) - return uri -} diff --git a/pkg/adapter/singbox/anytls.go b/pkg/adapter/singbox/anytls.go deleted file mode 100644 index 700f0d9..0000000 --- a/pkg/adapter/singbox/anytls.go +++ /dev/null @@ -1,42 +0,0 @@ -package singbox - -import "github.com/perfect-panel/server/pkg/adapter/proxy" - -type AnyTLSOutboundOptions struct { - ServerOptions - OutboundTLSOptionsContainer - Password string `json:"password,omitempty"` -} - -func ParseAnyTLS(data proxy.Proxy, password string) (*Proxy, error) { - anyTLS := data.Option.(proxy.AnyTLS) - - config := &AnyTLSOutboundOptions{ - ServerOptions: ServerOptions{ - Tag: data.Name, - Type: AnyTLS, - Server: data.Server, - ServerPort: data.Port, - }, - OutboundTLSOptionsContainer: OutboundTLSOptionsContainer{ - TLS: &OutboundTLSOptions{ - Enabled: true, - ALPN: []string{"h2", "http/1.1"}, - Insecure: anyTLS.SecurityConfig.AllowInsecure, - }, - }, - Password: password, - } - - if anyTLS.SecurityConfig.SNI != "" { - config.OutboundTLSOptionsContainer.TLS.ServerName = anyTLS.SecurityConfig.SNI - } - - p := &Proxy{ - Tag: data.Name, - Type: AnyTLS, - AnyTLSOptions: config, - } - - return p, nil -} diff --git a/pkg/adapter/singbox/build.go b/pkg/adapter/singbox/build.go deleted file mode 100644 index 8962c2b..0000000 --- a/pkg/adapter/singbox/build.go +++ /dev/null @@ -1,201 +0,0 @@ -package singbox - -import ( - "encoding/json" - - "github.com/perfect-panel/server/pkg/adapter/proxy" - "github.com/perfect-panel/server/pkg/logger" -) - -func BuildSingbox(adapter proxy.Adapter, uuid string) ([]byte, error) { - // build outbounds type is Proxy - var proxies []Proxy - // build outbound group - for _, group := range adapter.Group { - if group.Type == proxy.GroupTypeSelect { - selector := Proxy{ - Type: Selector, - Tag: group.Name, - SelectorOptions: &SelectorOutboundOptions{ - OutboundOptions: OutboundOptions{ - Tag: group.Name, - Type: Selector, - }, - Outbounds: group.Proxies, - Default: group.Proxies[0], - InterruptExistConnections: false, - }, - } - proxies = append(proxies, selector) - } else if group.Type == proxy.GroupTypeURLTest { - selector := Proxy{ - Type: URLTest, - Tag: group.Name, - URLTestOptions: &URLTestOutboundOptions{ - OutboundOptions: OutboundOptions{ - Tag: group.Name, - Type: URLTest, - }, - Outbounds: group.Proxies, - URL: group.URL, - }, - } - proxies = append(proxies, selector) - } else { - logger.Errorf("[sing-box] Unknown group type: %s, group name: %s", group.Type, group.Name) - } - } - - // build outbounds - for _, data := range adapter.Proxies { - p := buildProxy(data, uuid) - if p == nil { - continue - } - proxies = append(proxies, *p) - } - - // add direct outbound - direct := Proxy{ - Type: Direct, - Tag: "DIRECT", - } - // add block outbound - block := Proxy{ - Type: Block, - Tag: "block", - } - // add dns outbound - dns := Proxy{ - Type: DNS, - Tag: "dns-out", - } - proxies = append(proxies, direct, block, dns) - - var rawConfig map[string]any - if err := json.Unmarshal([]byte(DefaultTemplate), &rawConfig); err != nil { - return nil, err - } - - rawConfig["outbounds"] = proxies - route := RouteOptions{ - Final: adapter.Default, - Rules: []Rule{ - { - Inbound: []string{ - "tun-in", - "mixed-in", - }, - Action: "sniff", - }, - { - Type: "logical", - Mode: "or", - Rules: []Rule{ - { - Port: []uint16{53}, - }, - { - Protocol: []string{"dns"}, - }, - }, - Action: "hijack-dns", - }, - { - RuleSet: []string{ - "geosite-category-ads-all", - }, - ClashMode: "rule", - Action: "reject", - }, - { - ClashMode: "direct", - Outbound: "DIRECT", - }, - { - ClashMode: "global", - Outbound: adapter.Default, - }, - { - IPIsPrivate: true, - Outbound: "DIRECT", - }, - { - RuleSet: []string{ - "geosite-private", - }, - Outbound: "DIRECT", - }, - }, - RuleSet: []RuleSet{ - { - Tag: "geoip-cn", - Type: "remote", - Format: "binary", - URL: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geoip/cn.srs", - DownloadDetour: "DIRECT", - }, - { - Tag: "geosite-cn", - Type: "remote", - Format: "binary", - URL: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/cn.srs", - DownloadDetour: "DIRECT", - }, - { - Tag: "geosite-private", - Type: "remote", - Format: "binary", - URL: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/private.srs", - DownloadDetour: "DIRECT", - }, - { - Tag: "geosite-category-ads-all", - Type: "remote", - Format: "binary", - URL: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/category-ads-all.srs", - DownloadDetour: "DIRECT", - }, - { - Tag: "geosite-geolocation-!cn", - Type: "remote", - Format: "binary", - URL: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/geolocation-!cn.srs", - DownloadDetour: "DIRECT", - }, - }, - AutoDetectInterface: true, - } - route.Rules = append(route.Rules, adapterToSingboxRule(adapter.Rules)...) - rawConfig["route"] = route - return json.Marshal(rawConfig) -} - -func buildProxy(data proxy.Proxy, uuid string) *Proxy { - var p *Proxy - var err error - switch data.Protocol { - case VLESS: - p, err = ParseVless(data, uuid) - case Shadowsocks: - p, err = ParseShadowsocks(data, uuid) - case Trojan: - p, err = ParseTrojan(data, uuid) - case VMess: - p, err = ParseVMess(data, uuid) - - case Hysteria2: - p, err = ParseHysteria2(data, uuid) - - case TUIC: - p, err = ParseTUIC(data, uuid) - - default: - logger.Error("Unknown protocol", logger.Field("protocol", data.Protocol), logger.Field("server", data.Name)) - } - if err != nil { - logger.Error("ParseVless", logger.Field("error", err.Error()), logger.Field("server", data.Name), logger.Field("protocol", data.Protocol)) - return nil - } - return p -} diff --git a/pkg/adapter/singbox/default.go b/pkg/adapter/singbox/default.go deleted file mode 100644 index d73b236..0000000 --- a/pkg/adapter/singbox/default.go +++ /dev/null @@ -1,100 +0,0 @@ -package singbox - -const DefaultTemplate = ` -{ - "log": { - "level": "info", - "timestamp": true - }, - "experimental": { - "clash_api": { - "external_controller": "127.0.0.1:9090", - "external_ui": "ui", - "secret": "", - "external_ui_download_url": "https://mirror.ghproxy.com/https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip", - "external_ui_download_detour": "direct", - "default_mode": "rule" - }, - "cache_file": { - "enabled": true, - "store_fakeip": false - } - }, - "dns": { - "servers": [ - { - "tag": "dns_proxy", - "address": "tls://8.8.8.8", - "detour": "手动选择" - }, - { - "tag": "dns_direct", - "address": "https://223.5.5.5/dns-query", - "detour": "DIRECT" - } - ], - "rules": [ - { - "outbound": "any", - "server": "dns_direct", - "disable_cache": true - }, - { - "rule_set": "geosite-cn", - "server": "dns_direct" - }, - { - "clash_mode": "direct", - "server": "dns_direct" - }, - { - "clash_mode": "global", - "server": "dns_proxy" - }, - { - "rule_set": "geosite-geolocation-!cn", - "server": "dns_proxy" - } - ], - "final": "dns_direct", - "strategy": "ipv4_only" - }, - "route": { - "rules": [ - { - "action": "sniff" - }, - { - "protocol": "dns", - "action": "hijack-dns" - } - ] - }, - "inbounds": [ - { - "tag": "tun-in", - "type": "tun", - "address": [ - "172.18.0.1/30", - "fdfe:dcba:9876::1/126" - ], - "auto_route": true, - "strict_route": true, - "stack": "system", - "platform": { - "http_proxy": { - "enabled": true, - "server": "127.0.0.1", - "server_port": 7890 - } - } - }, - { - "tag": "mixed-in", - "type": "mixed", - "listen": "127.0.0.1", - "listen_port": 7890 - } - ] -} -` diff --git a/pkg/adapter/singbox/hysteria2.go b/pkg/adapter/singbox/hysteria2.go deleted file mode 100644 index b5ab2ff..0000000 --- a/pkg/adapter/singbox/hysteria2.go +++ /dev/null @@ -1,76 +0,0 @@ -package singbox - -import ( - "strings" - - "github.com/perfect-panel/server/pkg/adapter/proxy" -) - -type Hysteria2Obfs struct { - Type string `json:"type,omitempty"` - Password string `json:"password,omitempty"` -} - -type Hysteria2OutboundOptions struct { - ServerOptions - ServerPorts []string `json:"server_ports,omitempty"` - HopInterval int `json:"hop_interval,omitempty"` - UpMbps int `json:"up_mbps,omitempty"` - DownMbps int `json:"down_mbps,omitempty"` - Obfs *Hysteria2Obfs `json:"obfs,omitempty"` - Password string `json:"password,omitempty"` - Network string `json:"network,omitempty"` - OutboundTLSOptionsContainer - Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` - Transport *V2RayTransportOptions `json:"transport,omitempty"` -} - -func ParseHysteria2(data proxy.Proxy, password string) (*Proxy, error) { - hysteria2 := data.Option.(proxy.Hysteria2) - - p := &Proxy{ - Tag: data.Name, - Type: Hysteria2, - Hysteria2Options: &Hysteria2OutboundOptions{ - ServerOptions: ServerOptions{ - Tag: data.Name, - Type: Hysteria2, - Server: data.Server, - }, - Password: password, - }, - } - - var ports []string - - if hysteria2.HopPorts != "" { - ps := strings.Split(hysteria2.HopPorts, ",") - for _, port := range ps { - // 舍弃单个端口,只保留端口范围 - if len(strings.Split(port, "-")) > 1 { - tmp := strings.Split(port, "-") - ports = append(ports, strings.Join(tmp, ":")) - } - } - - } - if len(ports) > 0 { - p.Hysteria2Options.ServerPorts = ports - p.Hysteria2Options.HopInterval = hysteria2.HopInterval - } else { - p.Hysteria2Options.ServerPort = data.Port - } - - if hysteria2.ObfsPassword != "" { - p.Hysteria2Options.Obfs = &Hysteria2Obfs{ - Type: "salamander", - Password: hysteria2.ObfsPassword, - } - } - var tls *OutboundTLSOptions - if hysteria2.SecurityConfig.SNI != "" { - tls = NewOutboundTLSOptions("tls", hysteria2.SecurityConfig) - } - p.Hysteria2Options.TLS = tls - return p, nil -} diff --git a/pkg/adapter/singbox/multiplex.go b/pkg/adapter/singbox/multiplex.go deleted file mode 100644 index 7188f95..0000000 --- a/pkg/adapter/singbox/multiplex.go +++ /dev/null @@ -1,17 +0,0 @@ -package singbox - -type OutboundMultiplexOptions struct { - Enabled bool `json:"enabled,omitempty"` - Protocol string `json:"protocol,omitempty"` - MaxConnections int `json:"max_connections,omitempty"` - MinStreams int `json:"min_streams,omitempty"` - MaxStreams int `json:"max_streams,omitempty"` - Padding bool `json:"padding,omitempty"` - Brutal *BrutalOptions `json:"brutal,omitempty"` -} - -type BrutalOptions struct { - Enabled bool `json:"enabled,omitempty"` - UpMbps int `json:"up_mbps,omitempty"` - DownMbps int `json:"down_mbps,omitempty"` -} diff --git a/pkg/adapter/singbox/rule.go b/pkg/adapter/singbox/rule.go deleted file mode 100644 index a6446b1..0000000 --- a/pkg/adapter/singbox/rule.go +++ /dev/null @@ -1,130 +0,0 @@ -package singbox - -import ( - "strconv" - - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/rules" -) - -type Rule struct { - Outbound string `json:"outbound,omitempty"` - ClashMode string `json:"clash_mode,omitempty"` - RuleSet []string `json:"rule_set,omitempty"` - Domain []string `json:"domain,omitempty"` - DomainSuffix []string `json:"domain_suffix,omitempty"` - DomainKeyword []string `json:"domain_keyword,omitempty"` - DomainRegex []string `json:"domain_regex,omitempty"` - GeoIP []string `json:"geoip,omitempty"` - IPCIDR []string `json:"ip_cidr,omitempty"` - IPIsPrivate bool `json:"ip_is_private,omitempty"` - SourceIPCIDR []string `json:"source_ip_cidr,omitempty"` - ProcessName []string `json:"process_name,omitempty"` - ProcessPath []string `json:"process_path,omitempty"` - SourcePort []uint16 `json:"source_port,omitempty"` - Protocol []string `json:"protocol,omitempty"` - Port []uint16 `json:"port,omitempty"` - Action string `json:"action,omitempty"` - Inbound []string `json:"inbound,omitempty"` - Rules []Rule `json:"rules,omitempty"` - Type string `json:"type,omitempty"` - Mode string `json:"mode,omitempty"` -} - -type RuleSet struct { - Tag string `json:"tag,omitempty"` - Type string `json:"type,omitempty"` - Format string `json:"format,omitempty"` - URL string `json:"url,omitempty"` - DownloadDetour string `json:"download_detour,omitempty"` -} - -func adapterToSingboxRule(texts []string) []Rule { - var rulesList []Rule - for _, rule := range texts { - r := rules.NewRule(rule, "") - if r == nil { - continue - } - rulesList = addRuleToItem(rulesList, r.Target, *r) - } - return rulesList -} - -func addRuleToItem(group []Rule, outbound string, rule rules.Rule) []Rule { - for i := range group { - if group[i].Outbound == outbound { - switch rules.ParseRuleType(rule.Type) { - case rules.Domain: - group[i].Domain = append(group[i].Domain, rule.Payload) - return group - case rules.DomainSuffix: - group[i].DomainSuffix = append(group[i].DomainSuffix, rule.Payload) - return group - case rules.DomainKeyword: - group[i].DomainKeyword = append(group[i].DomainKeyword, rule.Payload) - return group - case rules.IPCIDR: - group[i].IPCIDR = append(group[i].IPCIDR, rule.Payload) - return group - case rules.SrcIPCIDR: - group[i].SourceIPCIDR = append(group[i].SourceIPCIDR, rule.Payload) - return group - case rules.SrcPort: - port, err := strconv.ParseUint(rule.Payload, 10, 16) - if err != nil { - logger.Errorf("[adapterToSingboxRule] failed to parse port %s to uint16", rule.Payload) - return group - } - group[i].SourcePort = append(group[i].SourcePort, uint16(port)) - return group - case rules.GEOIP: - group[i].GeoIP = append(group[i].GeoIP, rule.Payload) - return group - case rules.Process: - group[i].ProcessName = append(group[i].ProcessName, rule.Payload) - return group - case rules.ProcessPath: - group[i].ProcessPath = append(group[i].ProcessPath, rule.Payload) - return group - default: - logger.Errorf("[adapterToSingboxRule] unknown rule type %s", rule.Type) - return group - } - } - } - newRule := Rule{ - Outbound: outbound, - } - - switch rules.ParseRuleType(rule.Type) { - case rules.Domain: - newRule.Domain = []string{rule.Payload} - case rules.DomainSuffix: - newRule.DomainSuffix = []string{rule.Payload} - case rules.DomainKeyword: - newRule.DomainKeyword = []string{rule.Payload} - case rules.IPCIDR: - newRule.IPCIDR = []string{rule.Payload} - case rules.SrcIPCIDR: - newRule.SourceIPCIDR = []string{rule.Payload} - case rules.SrcPort: - port, err := strconv.ParseUint(rule.Payload, 10, 16) - if err != nil { - logger.Errorf("[adapterToSingboxRule] failed to parse port %s to uint16", rule.Payload) - return group - } - newRule.SourcePort = []uint16{uint16(port)} - case rules.GEOIP: - newRule.GeoIP = []string{rule.Payload} - case rules.Process: - newRule.ProcessName = []string{rule.Payload} - case rules.ProcessPath: - newRule.ProcessPath = []string{rule.Payload} - default: - logger.Errorf("[adapterToSingboxRule] unknown rule type %s", rule.Type) - return group - } - group = append(group, newRule) - return group -} diff --git a/pkg/adapter/singbox/rule_test.go b/pkg/adapter/singbox/rule_test.go deleted file mode 100644 index 95f8ce3..0000000 --- a/pkg/adapter/singbox/rule_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package singbox - -import ( - "fmt" - "testing" -) - -func TestAdapterToSingboxRule(t *testing.T) { - rules := []string{ - "DOMAIN,example.com,DIRECT", - "DOMAIN-SUFFIX,google.com,智能线路", - } - result := adapterToSingboxRule(rules) - fmt.Printf("TestAdapterToSingboxRule: result: %+v\n", result) -} diff --git a/pkg/adapter/singbox/shadowsocks.go b/pkg/adapter/singbox/shadowsocks.go deleted file mode 100644 index fbc8db8..0000000 --- a/pkg/adapter/singbox/shadowsocks.go +++ /dev/null @@ -1,45 +0,0 @@ -package singbox - -import ( - "fmt" - "strings" - - "github.com/perfect-panel/server/pkg/adapter/proxy" -) - -type ShadowsocksOptions struct { - ServerOptions - Method string `json:"method,omitempty"` - Password string `json:"password,omitempty"` - Plugin string `json:"plugin,omitempty"` - PluginOptions string `json:"plugin_opts,omitempty"` - Network string `json:"network,omitempty"` -} - -func ParseShadowsocks(data proxy.Proxy, uuid string) (*Proxy, error) { - ss := data.Option.(proxy.Shadowsocks) - - password := uuid - // SIP022 AEAD-2022 Ciphers - if strings.Contains(ss.Method, "2022") { - serverKey, userKey := proxy.GenerateShadowsocks2022Password(ss, uuid) - password = fmt.Sprintf("%s:%s", serverKey, userKey) - } - - p := &Proxy{ - Tag: data.Name, - Type: Shadowsocks, - ShadowsocksOptions: &ShadowsocksOptions{ - ServerOptions: ServerOptions{ - Tag: data.Name, - Type: Shadowsocks, - Server: data.Server, - ServerPort: data.Port, - }, - Method: ss.Method, - Password: password, - Network: "tcp", - }, - } - return p, nil -} diff --git a/pkg/adapter/singbox/singbox.go b/pkg/adapter/singbox/singbox.go deleted file mode 100644 index 7957f48..0000000 --- a/pkg/adapter/singbox/singbox.go +++ /dev/null @@ -1,102 +0,0 @@ -package singbox - -import ( - "encoding/json" - "fmt" -) - -const ( - Trojan = "trojan" - VLESS = "vless" - VMess = "vmess" - TUIC = "tuic" - Hysteria2 = "hysteria2" - AnyTLS = "anytls" - Shadowsocks = "shadowsocks" - Selector = "selector" - URLTest = "urltest" - Direct = "direct" - Block = "block" - DNS = "dns" -) - -type Proxy struct { - Tag string `json:"tag,omitempty"` - Type string `json:"type"` - ShadowsocksOptions *ShadowsocksOptions `json:"-"` - TUICOptions *TUICOutboundOptions `json:"-"` - TrojanOptions *TrojanOutboundOptions `json:"-"` - VLESSOptions *VLESSOutboundOptions `json:"-"` - VMessOptions *VMessOutboundOptions `json:"-"` - AnyTLSOptions *AnyTLSOutboundOptions `json:"-"` - Hysteria2Options *Hysteria2OutboundOptions `json:"-"` - SelectorOptions *SelectorOutboundOptions `json:"-"` - URLTestOptions *URLTestOutboundOptions `json:"-"` -} - -type ServerOptions struct { - Tag string `json:"tag"` - Type string `json:"type"` - Server string `json:"server"` - ServerPort int `json:"server_port,omitempty"` -} -type OutboundOptions struct { - Tag string `json:"tag"` - Type string `json:"type"` -} -type SelectorOutboundOptions struct { - OutboundOptions - Outbounds []string `json:"outbounds"` - Default string `json:"default,omitempty"` - InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` -} - -type URLTestOutboundOptions struct { - OutboundOptions - Outbounds []string `json:"outbounds"` - URL string `json:"url,omitempty"` - Interval Duration `json:"interval,omitempty"` - Tolerance uint16 `json:"tolerance,omitempty"` - IdleTimeout Duration `json:"idle_timeout,omitempty"` - InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` -} - -type RouteOptions struct { - Rules []Rule `json:"rules,omitempty"` - Final string `json:"final,omitempty"` - RuleSet []RuleSet `json:"rule_set,omitempty"` - AutoDetectInterface bool `json:"auto_detect_interface,omitempty"` -} - -func (p Proxy) MarshalJSON() ([]byte, error) { - type Alias Proxy - aux := struct { - Alias - }{ - Alias: (Alias)(p), - } - switch p.Type { - case Shadowsocks: - return json.Marshal(p.ShadowsocksOptions) - case TUIC: - return json.Marshal(p.TUICOptions) - case Trojan: - return json.Marshal(p.TrojanOptions) - case VLESS: - return json.Marshal(p.VLESSOptions) - case VMess: - return json.Marshal(p.VMessOptions) - case Hysteria2: - return json.Marshal(p.Hysteria2Options) - case AnyTLS: - return json.Marshal(p.AnyTLSOptions) - case Selector: - return json.Marshal(p.SelectorOptions) - case URLTest: - return json.Marshal(p.URLTestOptions) - case Direct, Block, DNS: - return json.Marshal(aux.Alias) - default: - return nil, fmt.Errorf("[sing-box] MarshalJSON unknown type: %s", p.Type) - } -} diff --git a/pkg/adapter/singbox/singbox_test.go b/pkg/adapter/singbox/singbox_test.go deleted file mode 100644 index e93584e..0000000 --- a/pkg/adapter/singbox/singbox_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package singbox - -import ( - "testing" - - "github.com/perfect-panel/server/pkg/adapter/proxy" - - "github.com/stretchr/testify/assert" -) - -func createSS() proxy.Proxy { - c := proxy.Shadowsocks{ - Method: "aes-256-gcm", - Port: 10301, - ServerKey: "", - } - return proxy.Proxy{ - Name: "Shadowsocks", - Server: "127.0.0.1", - Port: 10301, - Protocol: "shadowsocks", - Option: c, - } -} - -func createVLESS() proxy.Proxy { - c := proxy.Vless{ - Port: 10301, - Flow: "xtls-rprx-direct", - Transport: "websocket", - TransportConfig: proxy.TransportConfig{ - Path: "/ws", - Host: "baidu.com", - }, - Security: "tls", - SecurityConfig: proxy.SecurityConfig{ - SNI: "baidu.com", - Fingerprint: "chrome", - AllowInsecure: true, - }, - } - s := proxy.Proxy{ - Name: "VLESS", - Server: "test.xxx.com", - Port: 10301, - Protocol: "vless", - Option: c, - } - return s -} - -func TestSingboxShadowsocks(t *testing.T) { - s := createSS() - p, err := ParseShadowsocks(s, "uuid") - if err != nil { - t.Fatal(err) - } - data, err := p.MarshalJSON() - if err != nil { - t.Fatal(err) - } - assert.NotEqual(t, 0, len(data)) - - // Output: - // proxy: proxy: {"tag":"Shadowsocks","type":"shadowsocks","server":"127.0.0.1","server_port":10301,"method":"aes-256-gcm","password":"uuid","network":"tcp"} - -} - -func TestSingboxVless(t *testing.T) { - s := createVLESS() - p, err := ParseVless(s, "uuid") - if err != nil { - t.Fatal(err) - } - data, err := p.MarshalJSON() - if err != nil { - t.Fatal(err) - } - assert.NotEqual(t, 0, len(data)) -} diff --git a/pkg/adapter/singbox/tls.go b/pkg/adapter/singbox/tls.go deleted file mode 100644 index 84d1713..0000000 --- a/pkg/adapter/singbox/tls.go +++ /dev/null @@ -1,87 +0,0 @@ -package singbox - -import ( - "github.com/perfect-panel/server/pkg/adapter/proxy" -) - -type OutboundTLSOptions struct { - Enabled bool `json:"enabled,omitempty"` - DisableSNI bool `json:"disable_sni,omitempty"` - ServerName string `json:"server_name,omitempty"` - Insecure bool `json:"insecure,omitempty"` - ALPN Listable[string] `json:"alpn,omitempty"` - MinVersion string `json:"min_version,omitempty"` - MaxVersion string `json:"max_version,omitempty"` - CipherSuites Listable[string] `json:"cipher_suites,omitempty"` - Certificate Listable[string] `json:"certificate,omitempty"` - CertificatePath string `json:"certificate_path,omitempty"` - ECH *OutboundECHOptions `json:"ech,omitempty"` - UTLS *OutboundUTLSOptions `json:"utls,omitempty"` - Reality *OutboundRealityOptions `json:"reality,omitempty"` -} - -func NewOutboundTLSOptions(security string, cfg proxy.SecurityConfig) *OutboundTLSOptions { - var tls = &OutboundTLSOptions{} - switch security { - case "none": - return nil - case "tls": - tls.Enabled = true - if cfg.SNI != "" { - tls.ServerName = cfg.SNI - } else { - tls.DisableSNI = true - } - tls.Insecure = cfg.AllowInsecure - if cfg.Fingerprint != "" { - tls.UTLS = &OutboundUTLSOptions{ - Enabled: true, - Fingerprint: cfg.Fingerprint, - } - } - case "reality": - tls.Enabled = true - if cfg.SNI != "" { - tls.ServerName = cfg.SNI - } else { - tls.DisableSNI = true - } - tls.Insecure = cfg.AllowInsecure - if cfg.Fingerprint != "" { - tls.UTLS = &OutboundUTLSOptions{ - Enabled: true, - Fingerprint: cfg.Fingerprint, - } - } - tls.Reality = &OutboundRealityOptions{ - Enabled: true, - PublicKey: cfg.RealityPublicKey, - ShortID: cfg.RealityShortId, - } - } - return tls -} - -type OutboundECHOptions struct { - Enabled bool `json:"enabled,omitempty"` - PQSignatureSchemesEnabled bool `json:"pq_signature_schemes_enabled,omitempty"` - DynamicRecordSizingDisabled bool `json:"dynamic_record_sizing_disabled,omitempty"` - Config Listable[string] `json:"config,omitempty"` - ConfigPath string `json:"config_path,omitempty"` -} - -type OutboundRealityOptions struct { - Enabled bool `json:"enabled,omitempty"` - PublicKey string `json:"public_key,omitempty"` - ShortID string `json:"short_id,omitempty"` -} - -type OutboundUTLSOptions struct { - Enabled bool `json:"enabled,omitempty"` - Fingerprint string `json:"fingerprint,omitempty"` -} -type Listable[T any] []T - -type OutboundTLSOptionsContainer struct { - TLS *OutboundTLSOptions `json:"tls,omitempty"` -} diff --git a/pkg/adapter/singbox/tool.go b/pkg/adapter/singbox/tool.go deleted file mode 100644 index 535dbe8..0000000 --- a/pkg/adapter/singbox/tool.go +++ /dev/null @@ -1,11 +0,0 @@ -package singbox - -import "encoding/json" - -func mergeOptions(target map[string]any, options any) error { - optionsJSON, err := json.Marshal(options) - if err != nil { - return err - } - return json.Unmarshal(optionsJSON, &target) -} diff --git a/pkg/adapter/singbox/trojan.go b/pkg/adapter/singbox/trojan.go deleted file mode 100644 index 45eb756..0000000 --- a/pkg/adapter/singbox/trojan.go +++ /dev/null @@ -1,39 +0,0 @@ -package singbox - -import ( - "github.com/perfect-panel/server/pkg/adapter/proxy" -) - -type TrojanOutboundOptions struct { - ServerOptions - Password string `json:"password"` - Network string `json:"network,omitempty"` - OutboundTLSOptionsContainer - Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` - Transport *V2RayTransportOptions `json:"transport,omitempty"` -} - -func ParseTrojan(data proxy.Proxy, uuid string) (*Proxy, error) { - trojan := data.Option.(proxy.Trojan) - p := &Proxy{ - Tag: data.Name, - Type: Trojan, - TrojanOptions: &TrojanOutboundOptions{ - ServerOptions: ServerOptions{ - Tag: data.Name, - Type: Trojan, - Server: data.Server, - ServerPort: data.Port, - }, - Password: uuid, - }, - } - // Transport options - transport := NewV2RayTransportOptions(trojan.Transport, trojan.TransportConfig) - - p.TrojanOptions.Transport = transport - // Security options - p.TrojanOptions.TLS = NewOutboundTLSOptions(trojan.Security, trojan.SecurityConfig) - return p, nil - -} diff --git a/pkg/adapter/singbox/tuic.go b/pkg/adapter/singbox/tuic.go deleted file mode 100644 index 8267d40..0000000 --- a/pkg/adapter/singbox/tuic.go +++ /dev/null @@ -1,42 +0,0 @@ -package singbox - -import ( - "github.com/perfect-panel/server/pkg/adapter/proxy" -) - -type TUICOutboundOptions struct { - ServerOptions - UUID string `json:"uuid,omitempty"` - Password string `json:"password,omitempty"` - CongestionControl string `json:"congestion_control,omitempty"` - UDPRelayMode string `json:"udp_relay_mode,omitempty"` - UDPOverStream bool `json:"udp_over_stream,omitempty"` - ZeroRTTHandshake bool `json:"zero_rtt_handshake,omitempty"` - Heartbeat string `json:"heartbeat,omitempty"` - Network string `json:"network,omitempty"` - OutboundTLSOptionsContainer -} - -func ParseTUIC(data proxy.Proxy, uuid string) (*Proxy, error) { - tuic := data.Option.(proxy.Tuic) - p := &Proxy{ - Tag: data.Name, - Type: TUIC, - TUICOptions: &TUICOutboundOptions{ - ServerOptions: ServerOptions{ - Tag: data.Name, - Type: TUIC, - Server: data.Server, - ServerPort: data.Port, - }, - UUID: uuid, - Password: uuid, - CongestionControl: tuic.CongestionController, - UDPRelayMode: tuic.UDPRelayMode, - ZeroRTTHandshake: tuic.ReduceRtt, - }, - } - // Security options - p.TUICOptions.TLS = NewOutboundTLSOptions("tls", tuic.SecurityConfig) - return p, nil -} diff --git a/pkg/adapter/singbox/v2rayTransport.go b/pkg/adapter/singbox/v2rayTransport.go deleted file mode 100644 index 7d6b0a0..0000000 --- a/pkg/adapter/singbox/v2rayTransport.go +++ /dev/null @@ -1,114 +0,0 @@ -package singbox - -import ( - "encoding/json" - "time" - - "github.com/perfect-panel/server/pkg/adapter/proxy" -) - -type V2RayTransportOptions struct { - Type string `json:"type"` - HTTPOptions V2RayHTTPOptions `json:"-"` - WebsocketOptions V2RayWebsocketOptions `json:"-"` - QUICOptions V2RayQUICOptions `json:"-"` - GRPCOptions V2RayGRPCOptions `json:"-"` - HTTPUpgradeOptions V2RayHTTPUpgradeOptions `json:"-"` -} - -func (v V2RayTransportOptions) MarshalJSON() ([]byte, error) { - var v2rayTransportOptions any - data := map[string]any{ - "type": v.Type, - } - switch v.Type { - case "http": - v2rayTransportOptions = v.HTTPOptions - case "ws": - v2rayTransportOptions = v.WebsocketOptions - case "quic": - v2rayTransportOptions = v.QUICOptions - case "grpc": - v2rayTransportOptions = v.GRPCOptions - case "httpupgrade": - v2rayTransportOptions = v.HTTPUpgradeOptions - } - if err := mergeOptions(data, v2rayTransportOptions); err != nil { - return nil, err - } - return json.Marshal(data) -} - -func NewV2RayTransportOptions(network string, transport proxy.TransportConfig) *V2RayTransportOptions { - var t *V2RayTransportOptions = nil - switch network { - case "websocket": - t = &V2RayTransportOptions{ - Type: "ws", - WebsocketOptions: V2RayWebsocketOptions{ - Path: transport.Path, - Headers: map[string]Listable[string]{ - "Host": []string{transport.Host}, - }, - MaxEarlyData: 2048, - EarlyDataHeaderName: "Sec-WebSocket-Protocol", - }, - } - case "httpupgrade": - t = &V2RayTransportOptions{ - Type: "httpupgrade", - HTTPOptions: V2RayHTTPOptions{ - Path: transport.Path, - Host: []string{transport.Host}, - Headers: map[string]Listable[string]{ - "Host": []string{transport.Host}, - }, - }, - } - - case "grpc": - t = &V2RayTransportOptions{ - Type: "grpc", - GRPCOptions: V2RayGRPCOptions{ - ServiceName: transport.ServiceName, - }, - } - } - return t -} - -type V2RayHTTPOptions struct { - Host Listable[string] `json:"host,omitempty"` - Path string `json:"path,omitempty"` - Method string `json:"method,omitempty"` - Headers HTTPHeader `json:"headers,omitempty"` - IdleTimeout Duration `json:"idle_timeout,omitempty"` - PingTimeout Duration `json:"ping_timeout,omitempty"` -} - -type V2RayWebsocketOptions struct { - Path string `json:"path,omitempty"` - Headers HTTPHeader `json:"headers,omitempty"` - MaxEarlyData uint32 `json:"max_early_data,omitempty"` - EarlyDataHeaderName string `json:"early_data_header_name,omitempty"` -} - -type V2RayQUICOptions struct{} - -type V2RayGRPCOptions struct { - ServiceName string `json:"service_name,omitempty"` - IdleTimeout string `json:"idle_timeout,omitempty"` - PingTimeout string `json:"ping_timeout,omitempty"` - PermitWithoutStream bool `json:"permit_without_stream,omitempty"` - ForceLite bool `json:"-"` // for test -} - -type V2RayHTTPUpgradeOptions struct { - Host string `json:"host,omitempty"` - Path string `json:"path,omitempty"` - Headers HTTPHeader `json:"headers,omitempty"` -} - -type HTTPHeader map[string]Listable[string] - -type Duration time.Duration diff --git a/pkg/adapter/singbox/vless.go b/pkg/adapter/singbox/vless.go deleted file mode 100644 index 6370767..0000000 --- a/pkg/adapter/singbox/vless.go +++ /dev/null @@ -1,44 +0,0 @@ -package singbox - -import ( - "github.com/perfect-panel/server/pkg/adapter/proxy" -) - -type VLESSOutboundOptions struct { - ServerOptions - OutboundTLSOptionsContainer - UUID string `json:"uuid"` - Flow string `json:"flow,omitempty"` - Network string `json:"network,omitempty"` - Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` - Transport *V2RayTransportOptions `json:"transport,omitempty"` - PacketEncoding *string `json:"packet_encoding,omitempty"` -} - -func ParseVless(data proxy.Proxy, uuid string) (*Proxy, error) { - vless := data.Option.(proxy.Vless) - packetEncoding := "xudp" - p := &Proxy{ - Tag: data.Name, - Type: VLESS, - VLESSOptions: &VLESSOutboundOptions{ - ServerOptions: ServerOptions{ - Tag: data.Name, - Type: VLESS, - Server: data.Server, - ServerPort: data.Port, - }, - UUID: uuid, - Flow: vless.Flow, - PacketEncoding: &packetEncoding, - }, - } - // Transport options - transport := NewV2RayTransportOptions(vless.Transport, vless.TransportConfig) - p.VLESSOptions.Transport = transport - - // Security options - p.VLESSOptions.TLS = NewOutboundTLSOptions(vless.Security, vless.SecurityConfig) - - return p, nil -} diff --git a/pkg/adapter/singbox/vmess.go b/pkg/adapter/singbox/vmess.go deleted file mode 100644 index c9f316b..0000000 --- a/pkg/adapter/singbox/vmess.go +++ /dev/null @@ -1,43 +0,0 @@ -package singbox - -import ( - "github.com/perfect-panel/server/pkg/adapter/proxy" -) - -type VMessOutboundOptions struct { - ServerOptions - UUID string `json:"uuid"` - Security string `json:"security"` - AlterId int `json:"alter_id,omitempty"` - GlobalPadding bool `json:"global_padding,omitempty"` - AuthenticatedLength bool `json:"authenticated_length,omitempty"` - Network string `json:"network,omitempty"` - PacketEncoding string `json:"packet_encoding,omitempty"` - Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` - Transport *V2RayTransportOptions `json:"transport,omitempty"` - OutboundTLSOptionsContainer -} - -func ParseVMess(data proxy.Proxy, uuid string) (*Proxy, error) { - vmess := data.Option.(proxy.Vmess) - p := &Proxy{ - Type: VMess, - VMessOptions: &VMessOutboundOptions{ - ServerOptions: ServerOptions{ - Tag: data.Name, - Type: VMess, - Server: data.Server, - ServerPort: data.Port, - }, - UUID: uuid, - Security: "auto", - AlterId: 0, - }, - } - // Transport options - p.VMessOptions.Transport = NewV2RayTransportOptions(vmess.Transport, vmess.TransportConfig) - // Security options - p.VMessOptions.TLS = NewOutboundTLSOptions(vmess.Security, vmess.SecurityConfig) - - return p, nil -} diff --git a/pkg/adapter/surfboard/build.go b/pkg/adapter/surfboard/build.go deleted file mode 100644 index a481755..0000000 --- a/pkg/adapter/surfboard/build.go +++ /dev/null @@ -1,80 +0,0 @@ -package surfboard - -import ( - "bytes" - "embed" - "fmt" - "strings" - "text/template" - "time" - - "github.com/perfect-panel/server/pkg/adapter/proxy" - - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/tool" - "github.com/perfect-panel/server/pkg/traffic" -) - -//go:embed *.tpl -var configFiles embed.FS -var shadowsocksSupportMethod = []string{"aes-128-gcm", "aes-192-gcm", "aes-256-gcm", "chacha20-ietf-poly1305"} - -func BuildSurfboard(servers proxy.Adapter, siteName string, user UserInfo) []byte { - var proxies, proxyGroup string - var removed []string - var ps []string - - for _, p := range servers.Proxies { - switch p.Protocol { - case "shadowsocks": - proxies += buildShadowsocks(p, user.UUID) - case "trojan": - proxies += buildTrojan(p, user.UUID) - case "vmess": - proxies += buildVMess(p, user.UUID) - default: - removed = append(removed, p.Name) - } - ps = append(ps, p.Name) - } - - 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 - - var expiredAt string - if user.ExpiredDate.Before(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)) { - expiredAt = "长期有效" - } else { - expiredAt = user.ExpiredDate.Format("2006-01-02 15:04:05") - } - - ps = tool.RemoveStringElement(ps, removed...) - proxyGroup = strings.Join(ps, ",") - - // convert traffic - upload := traffic.AutoConvert(user.Upload, false) - download := traffic.AutoConvert(user.Download, false) - total := traffic.AutoConvert(user.TotalTraffic, false) - unusedTraffic := traffic.AutoConvert(user.TotalTraffic-user.Upload-user.Download, false) - // query Host - if err = tpl.Execute(&buf, map[string]interface{}{ - "Proxies": proxies, - "ProxyGroup": proxyGroup, - "SubscribeURL": user.SubscribeURL, - "SubscribeInfo": fmt.Sprintf("title=%s订阅信息, content=上传流量:%s\\n下载流量:%s\\n剩余流量: %s\\n套餐流量:%s\\n到期时间:%s", siteName, upload, download, unusedTraffic, total, expiredAt), - }); err != nil { - logger.Errorf("build Surge config error: %v", err.Error()) - return nil - } - return buf.Bytes() -} diff --git a/pkg/adapter/surfboard/build_test.go b/pkg/adapter/surfboard/build_test.go deleted file mode 100644 index 92bc086..0000000 --- a/pkg/adapter/surfboard/build_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package surfboard - -import ( - "testing" - "time" - - "github.com/perfect-panel/server/pkg/adapter/proxy" - - "github.com/perfect-panel/server/pkg/uuidx" -) - -func TestBuildSurfboard(t *testing.T) { - siteName := "test" - user := UserInfo{ - UUID: uuidx.NewUUID().String(), - Upload: 0, - Download: 0, - TotalTraffic: 0, - ExpiredDate: time.Now().AddDate(0, 1, 1), - SubscribeURL: "https://test.com", - } - conf := BuildSurfboard(proxy.Adapter{}, siteName, user) - t.Log(string(conf)) -} diff --git a/pkg/adapter/surfboard/default.tpl b/pkg/adapter/surfboard/default.tpl deleted file mode 100644 index 89c48fe..0000000 --- a/pkg/adapter/surfboard/default.tpl +++ /dev/null @@ -1,62 +0,0 @@ -#!MANAGED-CONFIG {{.SubscribeURL}} interval=43200 strict=true - -[General] -dns-server = system, 119.29.29.29, 223.5.5.5 -skip-proxy = 192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12, 127.0.0.0/8, localhost, *.local -always-real-ip = *.lan, lens.l.google.com, *.srv.nintendo.net, *.stun.playstation.net, *.xboxlive.com, xbox.*.*.microsoft.com, *.msftncsi.com, *.msftconnecttest.com -proxy-test-url = http://www.gstatic.com/generate_204 -internet-test-url = http://connectivitycheck.platform.hicloud.com/generate_204 -test-timeout = 5 -http-listen = 0.0.0.0:6088 -socks5-listen = 0.0.0.0:6089 - -[Panel] -SubscribeInfo = {{.SubscribeInfo}}, style=info - -[Proxy] -{{.Proxies}} - -[Proxy Group] -🚀 Proxy = select, 🌏 Auto, 🎯 Direct, include-other-group=🇺🇳 Nodes -🍎 Apple = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes -🔍 Google = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes -🪟 Microsoft = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes -📺 GlobalMedia = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes -🤖 AI = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes -🪙 Crypto = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes -🎮 Game = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes -📟 Telegram = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes -🇨🇳 China = select, 🎯 Direct, 🚀 Proxy, include-other-group=🇺🇳 Nodes -🐠 Final = select, 🎯 Direct, 🚀 Proxy, include-other-group=🇺🇳 Nodes -🌏 Auto = fallback, include-other-group=🇺🇳 Nodes, url=http://www.gstatic.com/generate_204, interval=600, timeout=5 -🎯 Direct = select, DIRECT, hidden=1 -🇺🇳 Nodes = select, {{.ProxyGroup}}, hidden=1 - -[Rule] -RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Apple/Apple_All.list, 🍎 Apple -RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Google/Google.list, 🔍 Google -RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/GitHub/GitHub.list, 🪟 Microsoft -RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Microsoft/Microsoft.list, 🪟 Microsoft -RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/HBO/HBO.list, 📺 GlobalMedia -RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Disney/Disney.list, 📺 GlobalMedia -RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/TikTok/TikTok.list, 📺 GlobalMedia -RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Netflix/Netflix.list, 📺 GlobalMedia -RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/GlobalMedia/GlobalMedia_All_No_Resolve.list, 📺 GlobalMedia -RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Telegram/Telegram.list, 📟 Telegram -RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/OpenAI/OpenAI.list, 🤖 AI -RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Gemini/Gemini.list, 🤖 AI -RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Copilot/Copilot.list, 🤖 AI -RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Claude/Claude.list, 🤖 AI -RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Crypto/Crypto.list, 🪙 Crypto -RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Cryptocurrency/Cryptocurrency.list, 🪙 Crypto -RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Game/Game.list, 🎮 Game -RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Global/Global_All_No_Resolve.list, 🚀 Proxy -RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/ChinaMax/ChinaMax_All_No_Resolve.list, 🇨🇳 China -RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Lan/Lan.list, 🎯 Direct - -GEOIP, CN, 🇨🇳 China -FINAL, 🐠 Final, dns-failed - -[URL Rewrite] -^https?:\/\/(www.)?g\.cn https://www.google.com 302 -^https?:\/\/(www.)?google\.cn https://www.google.com 302 \ No newline at end of file diff --git a/pkg/adapter/surfboard/model.go b/pkg/adapter/surfboard/model.go deleted file mode 100644 index 29d53ba..0000000 --- a/pkg/adapter/surfboard/model.go +++ /dev/null @@ -1,12 +0,0 @@ -package surfboard - -import "time" - -type UserInfo struct { - UUID string - Upload int64 - Download int64 - TotalTraffic int64 - ExpiredDate time.Time - SubscribeURL string -} diff --git a/pkg/adapter/surfboard/shadowsocks.go b/pkg/adapter/surfboard/shadowsocks.go deleted file mode 100644 index 41f4808..0000000 --- a/pkg/adapter/surfboard/shadowsocks.go +++ /dev/null @@ -1,28 +0,0 @@ -package surfboard - -import ( - "fmt" - "strings" - - "github.com/perfect-panel/server/pkg/adapter/proxy" -) - -func buildShadowsocks(data proxy.Proxy, uuid string) string { - ss, ok := data.Option.(proxy.Shadowsocks) - if !ok { - return "" - } - // Not supporting SIP022 AEAD-2022 Ciphers - if strings.Contains(ss.Method, "2022") { - return "" - } - addr := fmt.Sprintf("%s=ss, %s, %d", data.Name, data.Server, data.Port) - config := []string{ - addr, - fmt.Sprintf("encrypt-method=%s", ss.Method), - fmt.Sprintf("password=%s", uuid), - "tfo=true", - "udp-relay=true", - } - return strings.Join(config, ",") + "\r\n" -} diff --git a/pkg/adapter/surfboard/shadowsocks_test.go b/pkg/adapter/surfboard/shadowsocks_test.go deleted file mode 100644 index 924bfd0..0000000 --- a/pkg/adapter/surfboard/shadowsocks_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package surfboard - -import ( - "testing" - - "github.com/perfect-panel/server/pkg/adapter/proxy" -) - -func createSS() proxy.Proxy { - return proxy.Proxy{ - Name: "Shadowsocks", - Server: "test.xxxx.com", - Port: 10301, - Protocol: "shadowsocks", - Option: proxy.Shadowsocks{ - Port: 10301, - Method: "aes-256-gcm", - ServerKey: "123456", - }, - } -} - -func TestShadowsocks(t *testing.T) { - node := createSS() - uuid := "123456" - shadowsocks := buildShadowsocks(node, uuid) - t.Log(shadowsocks) -} diff --git a/pkg/adapter/surfboard/trojan.go b/pkg/adapter/surfboard/trojan.go deleted file mode 100644 index a0bbc88..0000000 --- a/pkg/adapter/surfboard/trojan.go +++ /dev/null @@ -1,41 +0,0 @@ -package surfboard - -import ( - "strconv" - "strings" - - "github.com/perfect-panel/server/pkg/adapter/proxy" -) - -func buildTrojan(data proxy.Proxy, uuid string) string { - // $config = [ - // "{$server['name']}=trojan", - // "{$server['host']}", - // "{$server['port']}", - // "password={$password}", - // $protocol_settings['server_name'] ? "sni={$protocol_settings['server_name']}" : "", - // 'tfo=true', - // 'udp-relay=true' - //]; - trojan, ok := data.Option.(proxy.Trojan) - if !ok { - return "" - } - config := []string{ - data.Name + "=trojan", - data.Server, - strconv.Itoa(data.Port), - "password=" + uuid, - "tfo=true", - "udp-relay=true", - } - if trojan.SecurityConfig.SNI != "" { - config = append(config, "sni="+trojan.SecurityConfig.SNI) - } - if trojan.SecurityConfig.AllowInsecure { - config = append(config, "skip-cert-verify=true") - } else { - config = append(config, "skip-cert-verify=false") - } - return strings.Join(config, ",") + "\r\n" -} diff --git a/pkg/adapter/surfboard/trojan_test.go b/pkg/adapter/surfboard/trojan_test.go deleted file mode 100644 index 728910f..0000000 --- a/pkg/adapter/surfboard/trojan_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package surfboard - -import ( - "testing" - - "github.com/perfect-panel/server/pkg/adapter/proxy" -) - -func createTrojan() proxy.Proxy { - - return proxy.Proxy{ - Name: "Trojan", - Server: "test.xxxx.com", - Port: 13002, - Protocol: "trojan", - Option: proxy.Trojan{ - Port: 13002, - Transport: "websocket", - TransportConfig: proxy.TransportConfig{ - Path: "/ws", - Host: "baidu.com", - }, - SecurityConfig: proxy.SecurityConfig{ - SNI: "baidu.com", - AllowInsecure: true, - }, - }, - } -} - -func TestTrojan(t *testing.T) { - node := createTrojan() - uuid := "123456" - trojan := buildTrojan(node, uuid) - t.Log(trojan) -} diff --git a/pkg/adapter/surfboard/vmess.go b/pkg/adapter/surfboard/vmess.go deleted file mode 100644 index 931afb1..0000000 --- a/pkg/adapter/surfboard/vmess.go +++ /dev/null @@ -1,45 +0,0 @@ -package surfboard - -import ( - "fmt" - "strings" - - "github.com/perfect-panel/server/pkg/adapter/proxy" -) - -func buildVMess(data proxy.Proxy, uuid string) string { - vmess, ok := data.Option.(proxy.Vmess) - if !ok { - return "" - } - addr := fmt.Sprintf("%s=vmess, %s, %d", data.Name, data.Server, data.Port) - uriConfig := []string{ - addr, - fmt.Sprintf("username=%s", uuid), - "vmess-aead=true", - "tfo=true", - "udp-relay=true", - } - if vmess.Security == "tls" { - uriConfig = append(uriConfig, "tls=true") - if vmess.SecurityConfig.AllowInsecure { - uriConfig = append(uriConfig, "skip-cert-verify=true") - } else { - uriConfig = append(uriConfig, "skip-cert-verify=false") - } - if vmess.SecurityConfig.SNI != "" { - uriConfig = append(uriConfig, fmt.Sprintf("sni=%s", vmess.SecurityConfig.SNI)) - } - } - if vmess.Transport == "websocket" { - uriConfig = append(uriConfig, "ws=true") - if vmess.TransportConfig.Path != "" { - uriConfig = append(uriConfig, fmt.Sprintf("ws-path=%s", vmess.TransportConfig.Path)) - } - if vmess.TransportConfig.Host != "" { - uriConfig = append(uriConfig, fmt.Sprintf("ws-headers=Host:%s", vmess.TransportConfig.Host)) - } - } - - return strings.Join(uriConfig, ",") + "\r\n" -} diff --git a/pkg/adapter/surfboard/vmess_test.go b/pkg/adapter/surfboard/vmess_test.go deleted file mode 100644 index 1feb773..0000000 --- a/pkg/adapter/surfboard/vmess_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package surfboard - -import ( - "testing" - - "github.com/perfect-panel/server/pkg/adapter/proxy" -) - -func createVMess() proxy.Proxy { - - return proxy.Proxy{ - Name: "Vmess", - Server: "test.xxxx.com", - Port: 13002, - Protocol: "vmess", - Option: proxy.Vmess{ - Port: 13002, - Transport: "websocket", - TransportConfig: proxy.TransportConfig{ - Path: "/ws", - Host: "test.xx.com", - }, - Security: "none", - }, - } -} - -func TestVMess(t *testing.T) { - node := createVMess() - uuid := "123456" - p := buildVMess(node, uuid) - t.Log(p) -} diff --git a/pkg/adapter/surge/default.tpl b/pkg/adapter/surge/default.tpl deleted file mode 100644 index 6b70844..0000000 --- a/pkg/adapter/surge/default.tpl +++ /dev/null @@ -1,79 +0,0 @@ -#!MANAGED-CONFIG {{.SubscribeURL}} interval=43200 strict=true - -[General] -loglevel = notify -external-controller-access = purinio@0.0.0.0:6170 -exclude-simple-hostnames = true -show-error-page-for-reject = true -udp-priority = true -udp-policy-not-supported-behaviour = reject -ipv6 = true -ipv6-vif = auto -proxy-test-url = http://www.gstatic.com/generate_204 -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 - -# > Surge Mac Parameters -http-listen = 0.0.0.0:6088 -socks5-listen = 0.0.0.0:6089 - -# > Surge iOS Parameters -allow-wifi-access = true -allow-hotspot-access = true -wifi-access-http-port = 6088 -wifi-access-socks5-port = 6089 - -[Panel] -SubscribeInfo = {{.SubscribeInfo}}, style=info - -[Proxy] -{{.Proxies}} - -[Proxy Group] -🚀 Proxy = select, 🌏 Auto, 🎯 Direct, include-other-group=🇺🇳 Nodes -🍎 Apple = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes -🔍 Google = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes -🪟 Microsoft = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes -📺 GlobalMedia = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes -🤖 AI = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes -🪙 Crypto = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes -🎮 Game = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes -📟 Telegram = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes -🇨🇳 China = select, 🎯 Direct, 🚀 Proxy, include-other-group=🇺🇳 Nodes -🐠 Final = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes -🌏 Auto = smart, include-other-group=🇺🇳 Nodes -🎯 Direct = select, DIRECT, hidden=1 -🇺🇳 Nodes = select, {{.ProxyGroup}}, hidden=1 - -[Rule] -RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Apple/Apple_All.list, 🍎 Apple -RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Google/Google.list, 🔍 Google -RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/GitHub/GitHub.list, 🪟 Microsoft -RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Microsoft/Microsoft.list, 🪟 Microsoft -RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/HBO/HBO.list, 📺 GlobalMedia -RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Disney/Disney.list, 📺 GlobalMedia -RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/TikTok/TikTok.list, 📺 GlobalMedia -RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Netflix/Netflix.list, 📺 GlobalMedia -RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/GlobalMedia/GlobalMedia_All_No_Resolve.list, 📺 GlobalMedia -RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Telegram/Telegram.list, 📟 Telegram -RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/OpenAI/OpenAI.list, 🤖 AI -RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Gemini/Gemini.list, 🤖 AI -RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Copilot/Copilot.list, 🤖 AI -RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Claude/Claude.list, 🤖 AI -RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Crypto/Crypto.list, 🪙 Crypto -RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Cryptocurrency/Cryptocurrency.list, 🪙 Crypto -RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Game/Game.list, 🎮 Game -RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Global/Global_All_No_Resolve.list, 🚀 Proxy -RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/ChinaMax/ChinaMax_All_No_Resolve.list, 🇨🇳 China -RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Lan/Lan.list, 🎯 Direct - -GEOIP, CN, 🇨🇳 China -FINAL, 🐠 Final, dns-failed - -[URL Rewrite] -^https?:\/\/(www.)?g\.cn https://www.google.com 302 -^https?:\/\/(www.)?google\.cn https://www.google.com 302 \ No newline at end of file diff --git a/pkg/adapter/surge/hysteria2.go b/pkg/adapter/surge/hysteria2.go deleted file mode 100644 index 778d395..0000000 --- a/pkg/adapter/surge/hysteria2.go +++ /dev/null @@ -1,43 +0,0 @@ -package surge - -import ( - "fmt" - "strconv" - "strings" - - "github.com/perfect-panel/server/pkg/adapter/proxy" -) - -func buildHysteria2(data proxy.Proxy, uuid string) string { - hysteria2, ok := data.Option.(proxy.Hysteria2) - if !ok { - return "" - } - - var port int - if hysteria2.HopPorts != "" { - ports := strings.Split(hysteria2.HopPorts, ",") - p := ports[0] - if len(strings.Split(p, "-")) > 1 { - p = strings.Split(p, "-")[0] - } - port, _ = strconv.Atoi(p) - } else { - port = data.Port - } - - config := []string{ - fmt.Sprintf("%s=hysteria2,%s,%d", data.Name, data.Server, port), - "password=" + uuid, - "udp-relay=true", - } - if hysteria2.SecurityConfig.SNI != "" { - config = append(config, "sni="+hysteria2.SecurityConfig.SNI) - } - if hysteria2.SecurityConfig.AllowInsecure { - config = append(config, "skip-cert-verify=true") - } else { - config = append(config, "skip-cert-verify=false") - } - return strings.Join(config, ",") + "\r\n" -} diff --git a/pkg/adapter/surge/hysteria2_test.go b/pkg/adapter/surge/hysteria2_test.go deleted file mode 100644 index 6e0803b..0000000 --- a/pkg/adapter/surge/hysteria2_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package surge - -import ( - "testing" - - "github.com/perfect-panel/server/pkg/adapter/proxy" -) - -func TestBuildHysteria2(t *testing.T) { - tests := []struct { - name string - data proxy.Proxy - uuid string - expected string - }{ - { - name: "Valid Hysteria2 with HopPorts", - data: proxy.Proxy{ - Name: "test", - Server: "server.com", - Port: 443, - Option: proxy.Hysteria2{ - HopPorts: "1000-2000", - SecurityConfig: proxy.SecurityConfig{ - SNI: "example.com", - AllowInsecure: true, - }, - }, - }, - uuid: "test-uuid", - expected: "test=hysteria2,server.com,1000,password=test-uuid,udp-relay=true,sni=example.com,skip-cert-verify=true\r\n", - }, - { - name: "Valid Hysteria2 without HopPorts", - data: proxy.Proxy{ - Name: "test", - Server: "server.com", - Port: 443, - Option: proxy.Hysteria2{ - SecurityConfig: proxy.SecurityConfig{ - SNI: "example.com", - AllowInsecure: false, - }, - }, - }, - uuid: "test-uuid", - expected: "test=hysteria2,server.com,443,password=test-uuid,udp-relay=true,sni=example.com,skip-cert-verify=false\r\n", - }, - { - name: "Invalid Hysteria2 Option", - data: proxy.Proxy{ - Name: "test", - Server: "server.com", - Port: 443, - Option: nil, - }, - uuid: "test-uuid", - expected: "", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := buildHysteria2(tt.data, tt.uuid) - if result != tt.expected { - t.Errorf("expected %s, got %s", tt.expected, result) - } - }) - } -} diff --git a/pkg/adapter/surge/shadowsocks.go b/pkg/adapter/surge/shadowsocks.go deleted file mode 100644 index bab18be..0000000 --- a/pkg/adapter/surge/shadowsocks.go +++ /dev/null @@ -1,32 +0,0 @@ -package surge - -import ( - "fmt" - "strings" - - "github.com/perfect-panel/server/pkg/adapter/proxy" -) - -func buildShadowsocks(data proxy.Proxy, uuid string) string { - ss, ok := data.Option.(proxy.Shadowsocks) - if !ok { - return "" - } - - password := uuid - // SIP022 AEAD-2022 Ciphers - if strings.Contains(ss.Method, "2022") { - serverKey, userKey := proxy.GenerateShadowsocks2022Password(ss, uuid) - password = fmt.Sprintf("%s:%s", serverKey, userKey) - } - - addr := fmt.Sprintf("%s=ss, %s, %d", data.Name, data.Server, data.Port) - config := []string{ - addr, - fmt.Sprintf("encrypt-method=%s", ss.Method), - fmt.Sprintf("password=%s", password), - "tfo=true", - "udp-relay=true", - } - return strings.Join(config, ",") + "\r\n" -} diff --git a/pkg/adapter/surge/surge.go b/pkg/adapter/surge/surge.go deleted file mode 100644 index 5215389..0000000 --- a/pkg/adapter/surge/surge.go +++ /dev/null @@ -1,101 +0,0 @@ -package surge - -import ( - "bytes" - "embed" - "fmt" - "strings" - "text/template" - "time" - - "github.com/perfect-panel/server/pkg/adapter/proxy" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/tool" - "github.com/perfect-panel/server/pkg/traffic" -) - -//go:embed *.tpl -var configFiles embed.FS - -type UserInfo struct { - UUID string - Upload int64 - Download int64 - TotalTraffic int64 - ExpiredDate time.Time - SubscribeURL string -} - -type Surge struct { - Adapter proxy.Adapter - UUID string - User UserInfo -} - -func NewSurge(adapter proxy.Adapter) *Surge { - return &Surge{ - Adapter: adapter, - } -} - -func (m *Surge) Build(siteName string, user UserInfo) []byte { - var proxies, proxyGroup string - var removed []string - var ps []string - - for _, p := range m.Adapter.Proxies { - switch p.Protocol { - case "shadowsocks": - proxies += buildShadowsocks(p, user.UUID) - case "trojan": - proxies += buildTrojan(p, user.UUID) - case "hysteria2": - proxies += buildHysteria2(p, user.UUID) - case "vmess": - proxies += buildVMess(p, user.UUID) - default: - removed = append(removed, p.Name) - } - ps = append(ps, p.Name) - } - - 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 - - var expiredAt string - if user.ExpiredDate.Before(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)) { - expiredAt = "长期有效" - } else { - expiredAt = user.ExpiredDate.Format("2006-01-02 15:04:05") - } - - ps = tool.RemoveStringElement(ps, removed...) - proxyGroup = strings.Join(ps, ",") - - // convert traffic - upload := traffic.AutoConvert(user.Upload, false) - download := traffic.AutoConvert(user.Download, false) - total := traffic.AutoConvert(user.TotalTraffic, false) - unusedTraffic := traffic.AutoConvert(user.TotalTraffic-user.Upload-user.Download, false) - // query Host - if err := tpl.Execute(&buf, map[string]interface{}{ - "Proxies": proxies, - "ProxyGroup": proxyGroup, - "SubscribeURL": user.SubscribeURL, - "SubscribeInfo": fmt.Sprintf("title=%s订阅信息, content=上传流量:%s\\n下载流量:%s\\n剩余流量: %s\\n套餐流量:%s\\n到期时间:%s", siteName, upload, download, unusedTraffic, total, expiredAt), - }); err != nil { - logger.Errorf("build Surge config error: %v", err.Error()) - return nil - } - return buf.Bytes() -} diff --git a/pkg/adapter/surge/surge_test.go b/pkg/adapter/surge/surge_test.go deleted file mode 100644 index 8c7b4f3..0000000 --- a/pkg/adapter/surge/surge_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package surge - -import ( - "strings" - "testing" - "time" - - "github.com/perfect-panel/server/pkg/adapter/proxy" -) - -func TestSurgeBuild(t *testing.T) { - adapter := proxy.Adapter{ - Proxies: []proxy.Proxy{ - { - Name: "test-shadowsocks", - Protocol: "shadowsocks", - Server: "1.2.3.4", - Port: 8388, - Option: proxy.Shadowsocks{ - Method: "aes-256-gcm", - }, - }, - { - Name: "test-trojan", - Protocol: "trojan", - Server: "5.6.7.8", - Port: 443, - Option: proxy.Trojan{ - SecurityConfig: proxy.SecurityConfig{ - SNI: "example.com", - AllowInsecure: true, - }, - }, - }, - { - Name: "test-hysteria", - Protocol: "hysteria2", - Server: "1.1.1.1", - Port: 443, - Option: proxy.Hysteria2{ - HopPorts: "8080-8090", - HopInterval: 320, - SecurityConfig: proxy.SecurityConfig{ - SNI: "example.com", - AllowInsecure: true, - }, - }, - }, - }, - Group: []proxy.Group{ - { - Name: "test-group", - Type: proxy.GroupTypeSelect, - Proxies: []string{"test-shadowsocks", "test-trojan", "test-hysteria"}, - }, - { - Name: "手动选择", - Type: proxy.GroupTypeSelect, - Proxies: []string{"test-shadowsocks", "test-trojan", "test-hysteria"}, - }, - }, - Rules: []string{ - "DOMAIN-SUFFIX,example.com,DIRECT", - }, - } - - user := UserInfo{ - UUID: "test-uuid", - Upload: 1024, - Download: 2048, - TotalTraffic: 4096, - ExpiredDate: time.Now().Add(24 * time.Hour), - SubscribeURL: "http://example.com/subscribe", - } - - surge := NewSurge(adapter) - config := surge.Build("test-uuid", "TestSite", user) - - if config == nil { - t.Fatal("Expected non-nil config") - } - - configStr := string(config) - t.Logf("configStr: %v", configStr) - if !strings.Contains(configStr, "test-shadowsocks=ss") { - t.Errorf("Expected config to contain test-shadowsocks proxy") - } - if !strings.Contains(configStr, "test-trojan=trojan") { - t.Errorf("Expected config to contain test-trojan proxy") - } - if !strings.Contains(configStr, "test-group = select") { - t.Errorf("Expected config to contain test-group proxy group") - } - if !strings.Contains(configStr, "DOMAIN-SUFFIX,example.com,DIRECT") { - t.Errorf("Expected config to contain rule for example.com") - } -} diff --git a/pkg/adapter/surge/trojan.go b/pkg/adapter/surge/trojan.go deleted file mode 100644 index f8fc7d2..0000000 --- a/pkg/adapter/surge/trojan.go +++ /dev/null @@ -1,32 +0,0 @@ -package surge - -import ( - "strconv" - "strings" - - "github.com/perfect-panel/server/pkg/adapter/proxy" -) - -func buildTrojan(data proxy.Proxy, uuid string) string { - trojan, ok := data.Option.(proxy.Trojan) - if !ok { - return "" - } - config := []string{ - data.Name + "=trojan", - data.Server, - strconv.Itoa(data.Port), - "password=" + uuid, - "tfo=true", - "udp-relay=true", - } - if trojan.SecurityConfig.SNI != "" { - config = append(config, "sni="+trojan.SecurityConfig.SNI) - } - if trojan.SecurityConfig.AllowInsecure { - config = append(config, "skip-cert-verify=true") - } else { - config = append(config, "skip-cert-verify=false") - } - return strings.Join(config, ",") + "\r\n" -} diff --git a/pkg/adapter/surge/vmess.go b/pkg/adapter/surge/vmess.go deleted file mode 100644 index 28ea9bd..0000000 --- a/pkg/adapter/surge/vmess.go +++ /dev/null @@ -1,44 +0,0 @@ -package surge - -import ( - "fmt" - "strings" - - "github.com/perfect-panel/server/pkg/adapter/proxy" -) - -func buildVMess(data proxy.Proxy, uuid string) string { - vmess, ok := data.Option.(proxy.Vmess) - if !ok { - return "" - } - addr := fmt.Sprintf("%s=vmess, %s, %d", data.Name, data.Server, data.Port) - uriConfig := []string{ - addr, - fmt.Sprintf("username=%s", uuid), - "vmess-aead=true", - "tfo=true", - "udp-relay=true", - } - if vmess.Security == "tls" { - uriConfig = append(uriConfig, "tls=true") - if vmess.SecurityConfig.AllowInsecure { - uriConfig = append(uriConfig, "skip-cert-verify=true") - } else { - uriConfig = append(uriConfig, "skip-cert-verify=false") - } - if vmess.SecurityConfig.SNI != "" { - uriConfig = append(uriConfig, fmt.Sprintf("sni=%s", vmess.SecurityConfig.SNI)) - } - } - if vmess.Transport == "websocket" { - uriConfig = append(uriConfig, "ws=true") - if vmess.TransportConfig.Path != "" { - uriConfig = append(uriConfig, fmt.Sprintf("ws-path=%s", vmess.TransportConfig.Path)) - } - if vmess.TransportConfig.Host != "" { - uriConfig = append(uriConfig, fmt.Sprintf("ws-headers=Host:%s", vmess.TransportConfig.Host)) - } - } - return strings.Join(uriConfig, ",") + "\r\n" -} diff --git a/pkg/adapter/template/clash.tpl b/pkg/adapter/template/clash.tpl deleted file mode 100644 index 6d8dd36..0000000 --- a/pkg/adapter/template/clash.tpl +++ /dev/null @@ -1,51 +0,0 @@ -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}} \ No newline at end of file diff --git a/pkg/adapter/uilts.go b/pkg/adapter/uilts.go deleted file mode 100644 index 317da61..0000000 --- a/pkg/adapter/uilts.go +++ /dev/null @@ -1,312 +0,0 @@ -package adapter - -import ( - "encoding/json" - "fmt" - "log" - "strings" - - "github.com/perfect-panel/server/internal/model/server" - "github.com/perfect-panel/server/pkg/adapter/proxy" - "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/random" - "github.com/perfect-panel/server/pkg/tool" -) - -// addNode creates a new proxy node based on the provided server data and host/port. -func addNode(data *server.Server, host string, port int) *proxy.Proxy { - var option any - tags := strings.Split(data.Tags, ",") - if len(tags) > 0 { - tags = tool.RemoveDuplicateElements(tags...) - } - - node := proxy.Proxy{ - Name: data.Name, - Server: host, - Port: port, - Country: data.Country, - Protocol: data.Protocol, - Tags: tags, - } - switch data.Protocol { - case "shadowsocks": - var ss proxy.Shadowsocks - if err := json.Unmarshal([]byte(data.Config), &ss); err != nil { - return nil - } - if port == 0 { - node.Port = ss.Port - } - option = ss - case "vless": - var vless proxy.Vless - if err := json.Unmarshal([]byte(data.Config), &vless); err != nil { - return nil - } - if port == 0 { - node.Port = vless.Port - } - option = vless - case "vmess": - var vmess proxy.Vmess - if err := json.Unmarshal([]byte(data.Config), &vmess); err != nil { - return nil - } - if port == 0 { - node.Port = vmess.Port - } - option = vmess - case "trojan": - var trojan proxy.Trojan - if err := json.Unmarshal([]byte(data.Config), &trojan); err != nil { - return nil - } - if port == 0 { - node.Port = trojan.Port - } - option = trojan - case "hysteria2": - var hysteria2 proxy.Hysteria2 - if err := json.Unmarshal([]byte(data.Config), &hysteria2); err != nil { - return nil - } - if port == 0 { - node.Port = hysteria2.Port - } - option = hysteria2 - case "tuic": - var tuic proxy.Tuic - if err := json.Unmarshal([]byte(data.Config), &tuic); err != nil { - return nil - } - if port == 0 { - node.Port = tuic.Port - } - option = tuic - case "anytls": - var anytls proxy.AnyTLS - if err := json.Unmarshal([]byte(data.Config), &anytls); err != nil { - logger.Errorw("解析AnyTLS配置失败", logger.Field("error", err.Error()), logger.Field("node", data.Name)) - return nil - } - if port == 0 { - node.Port = anytls.Port - } - option = anytls - logger.Infow("成功处理AnyTLS节点", logger.Field("node", data.Name), logger.Field("port", anytls.Port)) - default: - fmt.Printf("[Error] 不支持的协议: %s", data.Protocol) - return nil - } - node.Option = option - return &node -} - -func adapterRules(groups []*server.RuleGroup) (proxyGroup []proxy.Group, rules []string, defaultGroup string) { - for _, group := range groups { - if group.Default { - log.Printf("[Debug] 规则组 %s 是默认组", group.Name) - defaultGroup = group.Name - } - switch group.Type { - case server.RuleGroupTypeReject: - proxyGroup = append(proxyGroup, proxy.Group{ - Name: group.Name, - Type: proxy.GroupTypeSelect, - Proxies: []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")...) - } - log.Printf("[Dapter] 生成规则组: %d", len(proxyGroup)) - return proxyGroup, tool.RemoveDuplicateElements(rules...), defaultGroup -} - -// generateDefaultGroup generates a default proxy group with auto-selection and manual selection options. -func generateDefaultGroup() (proxyGroup []proxy.Group) { - proxyGroup = append(proxyGroup, proxy.Group{ - Name: AutoSelect, - Type: proxy.GroupTypeURLTest, - Proxies: make([]string, 0), - URL: "https://www.gstatic.com/generate_204", - Interval: 300, - }) - - return proxyGroup -} - -func adapterProxies(servers []*server.Server) ([]proxy.Proxy, []string, map[string][]string) { - var proxies []proxy.Proxy - var tags = make(map[string][]string) - for _, node := range servers { - switch node.RelayMode { - case server.RelayModeAll: - var relays []server.NodeRelay - if err := json.Unmarshal([]byte(node.RelayNode), &relays); err != nil { - logger.Errorw("Unmarshal RelayNode", logger.Field("error", err.Error()), logger.Field("node", node.Name), logger.Field("relayNode", node.RelayNode)) - continue - } - for _, relay := range relays { - n := addNode(node, relay.Host, relay.Port) - if n == nil { - continue - } - if relay.Prefix != "" { - n.Name = relay.Prefix + n.Name - } - if node.Tags != "" { - t := tool.RemoveDuplicateElements(strings.Split(node.Tags, ",")...) - for _, tag := range t { - if tag != "" { - if _, ok := tags[tag]; !ok { - tags[tag] = []string{} - } - tags[tag] = append(tags[tag], n.Name) - } - } - } - - proxies = append(proxies, *n) - } - case server.RelayModeRandom: - var relays []server.NodeRelay - if err := json.Unmarshal([]byte(node.RelayNode), &relays); err != nil { - logger.Errorw("Unmarshal RelayNode", logger.Field("error", err.Error()), logger.Field("node", node.Name), logger.Field("relayNode", node.RelayNode)) - continue - } - randNum := random.RandomInRange(0, len(relays)-1) - relay := relays[randNum] - n := addNode(node, relay.Host, relay.Port) - if n == nil { - continue - } - if relay.Prefix != "" { - n.Name = relay.Prefix + node.Name - } - if node.Tags != "" { - t := tool.RemoveDuplicateElements(strings.Split(node.Tags, ",")...) - for _, tag := range t { - if tag != "" { - if _, ok := tags[tag]; !ok { - tags[tag] = []string{} - } - tags[tag] = append(tags[tag], n.Name) - } - } - } - proxies = append(proxies, *n) - default: - logger.Info("Not Relay Mode", logger.Field("node", node.Name), logger.Field("relayMode", node.RelayMode)) - n := addNode(node, node.ServerAddr, 0) - if n != nil { - if node.Tags != "" { - t := tool.RemoveDuplicateElements(strings.Split(node.Tags, ",")...) - for _, tag := range t { - if tag != "" { - if _, ok := tags[tag]; !ok { - tags[tag] = []string{} - } - tags[tag] = append(tags[tag], n.Name) - } - } - } - proxies = append(proxies, *n) - } - } - } - - var nodes []string - for _, p := range proxies { - nodes = append(nodes, p.Name) - } - - return proxies, tool.RemoveDuplicateElements(nodes...), tags -} - -// RemoveEmptyString 切片去除空值 -func RemoveEmptyString(arr []string) []string { - var result []string - for _, str := range arr { - if str != "" { - result = append(result, str) - } - } - return result -} - -// SortGroups sorts the provided slice of proxy groups by their names. -func SortGroups(groups []proxy.Group, nodes []string, tags map[string][]string, defaultName string) []proxy.Group { - var sortedGroups []proxy.Group - var defaultGroup, autoSelectGroup proxy.Group - // 在所有分组找到默认分组并将他放到第一个 - for _, group := range groups { - if group.Name == "" || group.Name == "DIRECT" || group.Name == "REJECT" { - continue - } - // 如果是默认分组 - if group.Default { - group.Proxies = append([]string{AutoSelect}, nodes...) - group.Proxies = append(group.Proxies, "DIRECT") - defaultGroup = group - continue - } - if group.Reject || group.Direct { - if defaultName != AutoSelect { - group.Proxies = append(group.Proxies, defaultName) - } - sortedGroups = append(sortedGroups, group) - continue - } - - if group.Name == AutoSelect { - group.Proxies = nodes - autoSelectGroup = group - continue - } - // Tags 分组 - if len(group.Tags) > 0 { - var proxies []string - for _, tag := range group.Tags { - if node, ok := tags[tag]; ok { - proxies = append(proxies, node...) - } - } - group.Proxies = append(tool.RemoveDuplicateElements(proxies...), AutoSelect, "DIRECT") - sortedGroups = append(sortedGroups, group) - continue - } - - group.Proxies = append([]string{AutoSelect}, nodes...) - group.Proxies = append(group.Proxies, "DIRECT") - group.Proxies = tool.RemoveElementBySlice(group.Proxies, group.Name) - sortedGroups = append(sortedGroups, group) - } - - if defaultGroup.Name != "" { - sortedGroups = append([]proxy.Group{defaultGroup}, sortedGroups...) - } - if autoSelectGroup.Name != "" && autoSelectGroup.Name != defaultGroup.Name { - sortedGroups = append(sortedGroups, autoSelectGroup) - } - - return sortedGroups - -} diff --git a/pkg/adapter/v2rayn/v2rayN.go b/pkg/adapter/v2rayn/v2rayN.go deleted file mode 100644 index be07d0a..0000000 --- a/pkg/adapter/v2rayn/v2rayN.go +++ /dev/null @@ -1,40 +0,0 @@ -package v2rayn - -import ( - "github.com/perfect-panel/server/pkg/adapter/general" - "github.com/perfect-panel/server/pkg/adapter/proxy" -) - -type v2rayShareLink struct { - Ps string `json:"ps"` - Add string `json:"add"` - Port string `json:"port"` - ID string `json:"id"` - Aid string `json:"aid"` - Net string `json:"net"` - Type string `json:"type"` - Host string `json:"host"` - SNI string `json:"sni"` - Path string `json:"path"` - TLS string `json:"tls"` - Flow string `json:"flow,omitempty"` - Alpn string `json:"alpn,omitempty"` - AllowInsecure bool `json:"allowInsecure,omitempty"` - Fingerprint string `json:"fp,omitempty"` - PublicKey string `json:"pbk,omitempty"` - ShortId string `json:"sid,omitempty"` - SpiderX string `json:"spx,omitempty"` - V string `json:"v"` -} -type V2rayN struct { - proxy.Adapter -} - -func NewV2rayN(adapter proxy.Adapter) *V2rayN { - return &V2rayN{ - Adapter: adapter, - } -} -func (m *V2rayN) Build(uuid string) []byte { - return general.GenerateBase64General(m.Adapter.Proxies, uuid) -} diff --git a/pkg/constant/context.go b/pkg/constant/context.go index 4023cd1..45c7f86 100644 --- a/pkg/constant/context.go +++ b/pkg/constant/context.go @@ -8,4 +8,5 @@ const ( CtxKeyRequestHost CtxKey = "requestHost" CtxKeyPlatform CtxKey = "platform" CtxKeyPayment CtxKey = "payment" + LoginType CtxKey = "loginType" ) diff --git a/pkg/constant/types.go b/pkg/constant/types.go index ea34b57..a2db39b 100644 --- a/pkg/constant/types.go +++ b/pkg/constant/types.go @@ -1,8 +1,6 @@ package constant -import ( - "encoding/json" -) +import "encoding/json" // Used for type cloning conversion const ( @@ -46,7 +44,17 @@ type TemporaryOrderInfo struct { InviteCode string `json:"invite_code,omitempty"` } -func (t TemporaryOrderInfo) Marshal() string { - value, _ := json.Marshal(t) - return string(value) +func (t *TemporaryOrderInfo) Unmarshal(data []byte) error { + type Alias TemporaryOrderInfo + aux := (*Alias)(t) + return json.Unmarshal(data, aux) +} + +func (t *TemporaryOrderInfo) Marshal() ([]byte, error) { + type Alias TemporaryOrderInfo + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(t), + }) } diff --git a/pkg/constant/version.go b/pkg/constant/version.go index 5370107..b9e58ed 100644 --- a/pkg/constant/version.go +++ b/pkg/constant/version.go @@ -2,6 +2,8 @@ package constant // Version PPanel version var ( - Version = "unknown version" - BuildTime = "unknown time" + Version = "unknown version" + BuildTime = "unknown time" + Repository = "https://github.com/perfect-panel/server" + ServiceName = "ApiService" ) diff --git a/pkg/email/manager.go b/pkg/email/manager.go new file mode 100644 index 0000000..8e4b8a3 --- /dev/null +++ b/pkg/email/manager.go @@ -0,0 +1,134 @@ +package email + +import ( + "context" + "sync" + "time" + + "github.com/perfect-panel/server/pkg/logger" + "gorm.io/gorm" +) + +var ( + Manager *WorkerManager // 全局调度器实例 + once sync.Once // 确保 Scheduler 只被初始化一次 + limit sync.RWMutex // 控制并发限制 +) + +type WorkerManager struct { + db *gorm.DB // 数据库连接 + sender Sender // 邮件发送器接口 + mutex sync.RWMutex // 读写互斥锁,确保线程安全 + workers map[int64]*Worker // 存储所有 Worker 实例 + cancels map[int64]context.CancelFunc // 存储每个 Worker 的取消函数 +} + +func NewWorkerManager(db *gorm.DB, sender Sender) *WorkerManager { + if Manager != nil { + return Manager + } + once.Do(func() { + Manager = &WorkerManager{ + db: db, + workers: make(map[int64]*Worker), + cancels: make(map[int64]context.CancelFunc), + sender: sender, + } + }) + // 设置定时检查任务 + go func() { + for { + // 每隔5分钟检查一次 + select { + case <-time.After(1 * time.Minute): + checkWorker() + continue + } + } + }() + return Manager +} + +// AddWorker 添加一个新的 Worker 实例 +func (m *WorkerManager) AddWorker(id int64) { + m.mutex.Lock() + defer m.mutex.Unlock() + if _, exists := m.workers[id]; !exists { + ctx, cancel := context.WithCancel(context.Background()) + worker := NewWorker(ctx, id, m.db, m.sender) + m.workers[id] = worker + m.cancels[id] = cancel + go worker.Start() + logger.Info("Batch Send Email", + logger.Field("message", "Added new worker"), + logger.Field("task_id", id), + ) + } else { + logger.Info("Batch Send Email", + logger.Field("message", "Worker already exists"), + logger.Field("task_id", id), + ) + } + +} + +// GetWorker 获取指定任务的 Worker 实例 +func (m *WorkerManager) GetWorker(id int64) *Worker { + m.mutex.RLock() + defer m.mutex.RUnlock() + if worker, exists := m.workers[id]; exists { + return worker + } else { + logger.Error("Batch Send Email", + logger.Field("message", "Worker not found"), + logger.Field("task_id", id), + ) + return nil + } +} + +// RemoveWorker 移除指定任务的 Worker 实例 +func (m *WorkerManager) RemoveWorker(id int64) { + m.mutex.Lock() + defer m.mutex.Unlock() + if _, exists := m.workers[id]; exists { + delete(m.workers, id) + if cancelFunc, ok := m.cancels[id]; ok { + cancelFunc() // 调用取消函数 + delete(m.cancels, id) + } + logger.Info("Batch Send Email", + logger.Field("message", "Removed worker"), + logger.Field("task_id", id), + ) + } else { + logger.Error("Batch Send Email", + logger.Field("message", "Worker not found for removal"), + logger.Field("task_id", id), + ) + } +} + +func checkWorker() { + if Manager == nil { + // 如果 Manager 未初始化,直接返回 + return + } + Manager.mutex.Lock() + defer Manager.mutex.Unlock() + for id, worker := range Manager.workers { + if worker.IsRunning() == 2 { + // 如果Worker已完成,移除它 + delete(Manager.workers, id) + if cancelFunc, ok := Manager.cancels[id]; ok { + cancelFunc() // 调用取消函数 + delete(Manager.cancels, id) + } + logger.Info("Batch Send Email", + logger.Field("message", "Removed completed worker"), + logger.Field("task_id", id), + ) + } + } + +} diff --git a/pkg/email/worker.go b/pkg/email/worker.go new file mode 100644 index 0000000..fd5aba4 --- /dev/null +++ b/pkg/email/worker.go @@ -0,0 +1,192 @@ +package email + +import ( + "context" + "encoding/json" + "time" + + "github.com/perfect-panel/server/internal/model/task" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "gorm.io/gorm" +) + +type ErrorInfo struct { + Error string `json:"error"` + Email string `json:"email"` + Time int64 `json:"time"` +} + +type Worker struct { + id int64 // 任务ID + db *gorm.DB // 数据库连接 + ctx context.Context // 上下文 + sender Sender // 邮件发送器接口 + status uint8 // 任务状态,0 表示未运行,1 表示运行中 2 表示已完成 +} + +func NewWorker(ctx context.Context, id int64, db *gorm.DB, sender Sender) *Worker { + return &Worker{ + id: id, + db: db, + ctx: ctx, + sender: sender, + } +} + +// GetID 获取Worker的任务ID +func (w *Worker) GetID() int64 { + return w.id +} + +// IsRunning 检查Worker是否正在运行 +func (w *Worker) IsRunning() uint8 { + return w.status +} + +// Start 启动Worker,开始处理任务 +func (w *Worker) Start() { + // 检查并发限制 + limit.Lock() + defer limit.Unlock() + tx := w.db.WithContext(w.ctx) + var taskInfo task.Task + if err := tx.Model(&task.Task{}).Where("id = ?", w.id).First(&taskInfo).Error; err != nil { + logger.Error("Batch Send Email", + logger.Field("message", "Failed to find task"), + logger.Field("error", err.Error()), + logger.Field("task_id", w.id), + ) + return + } + if taskInfo.Status != 0 { + logger.Error("Batch Send Email", + logger.Field("message", "Task already completed or in progress"), + logger.Field("task_id", w.id), + ) + return + } + + var scope task.EmailScope + if err := json.Unmarshal([]byte(taskInfo.Scope), &scope); err != nil { + logger.Error("Batch Send Email", + logger.Field("message", "Failed to parse task scope"), + logger.Field("error", err.Error()), + logger.Field("task_id", w.id), + ) + return + } + + if len(scope.Recipients) == 0 && len(scope.Additional) == 0 { + logger.Error("Batch Send Email", + logger.Field("message", "No recipients or additional emails provided"), + logger.Field("task_id", w.id), + ) + return + } + + var content task.EmailContent + if err := json.Unmarshal([]byte(taskInfo.Content), &content); err != nil { + logger.Error("Batch Send Email", + logger.Field("message", "Failed to parse task content"), + logger.Field("error", err.Error()), + logger.Field("task_id", w.id), + ) + return + } + + w.status = 1 // 设置状态为运行中 + var recipients []string + // 解析收件人 + if len(scope.Recipients) > 0 { + recipients = append(recipients, scope.Recipients...) + } + // 解析附加收件人 + if len(scope.Additional) > 0 { + recipients = append(recipients, scope.Additional...) + } + // 去重和清理空字符串 + recipients = tool.RemoveDuplicateElements(recipients...) + + if len(recipients) == 0 { + logger.Error("Batch Send Email", + logger.Field("message", "No valid recipients found"), + logger.Field("task_id", w.id), + ) + w.status = 2 // 设置状态为已完成 + return + } + + // 设置发送间隔时间 + var intervalTime time.Duration + if scope.Interval == 0 { + intervalTime = 1 * time.Second + } else { + intervalTime = time.Duration(scope.Interval) * time.Second + } + + var errors []ErrorInfo + var count uint64 + for _, recipient := range recipients { + select { + case <-w.ctx.Done(): + logger.Info("Batch Send Email", + logger.Field("message", "Worker stopped by context cancellation"), + logger.Field("task_id", w.id), + ) + return + default: + } + if taskInfo.Status == 0 { + taskInfo.Status = 1 // 1 表示任务进行中 + } + + if err := w.sender.Send([]string{recipient}, content.Subject, content.Content); err != nil { + logger.Error("Batch Send Email", + logger.Field("message", "Failed to send email"), + logger.Field("error", err.Error()), + logger.Field("recipient", recipient), + logger.Field("task_id", w.id), + ) + errors = append(errors, ErrorInfo{ + Error: err.Error(), + Email: recipient, + Time: time.Now().Unix(), + }) + text, _ := json.Marshal(errors) + taskInfo.Errors = string(text) + } + count++ + taskInfo.Current = count + if err := tx.Model(&task.Task{}).Where("`id` = ?", taskInfo.Id).Save(&taskInfo).Error; err != nil { + logger.Error("Batch Send Email", + logger.Field("message", "Failed to update task progress"), + logger.Field("error", err.Error()), + logger.Field("task_id", w.id), + ) + errors = append(errors, ErrorInfo{ + Error: err.Error(), + Email: recipient, + Time: time.Now().Unix(), + }) + w.status = 2 // 设置状态为已完成 + } + time.Sleep(intervalTime) + } + taskInfo.Status = 2 // 2 表示任务已完成 + w.status = 2 // 设置状态为已完成 + + if err := tx.Model(&task.Task{}).Where("`id` = ?", taskInfo.Id).Save(&taskInfo).Error; err != nil { + logger.Error("Batch Send Email", + logger.Field("message", "Failed to finalize task"), + logger.Field("error", err.Error()), + logger.Field("task_id", w.id), + ) + } else { + logger.Info("Batch Send Email", + logger.Field("message", "Task completed successfully"), + logger.Field("task_id", w.id), + logger.Field("total_sent", count), + ) + } +} diff --git a/pkg/nodeMultiplier/manage_test.go b/pkg/nodeMultiplier/manage_test.go index 789dae9..881db55 100644 --- a/pkg/nodeMultiplier/manage_test.go +++ b/pkg/nodeMultiplier/manage_test.go @@ -8,10 +8,15 @@ import ( func TestNewNodeMultiplierManager(t *testing.T) { periods := []TimePeriod{ { - StartTime: "23:00", - EndTime: "1:59", + StartTime: "23:00.000", + EndTime: "1:59.000", Multiplier: 1.2, }, + { + StartTime: "12:00.000", + EndTime: "13:59.000", + Multiplier: 0.5, + }, } m := NewNodeMultiplierManager(periods) if len(m.Periods) != 1 { diff --git a/pkg/nodeMultiplier/manager.go b/pkg/nodeMultiplier/manager.go index 7f9e687..7cb6d02 100644 --- a/pkg/nodeMultiplier/manager.go +++ b/pkg/nodeMultiplier/manager.go @@ -28,8 +28,8 @@ func (m *Manager) GetMultiplier(current time.Time) float32 { } func (m *Manager) isInTimePeriod(current time.Time, start, end string) bool { - startTime, _ := time.Parse("15:04", start) - endTime, _ := time.Parse("15:04", end) + startTime, _ := time.Parse("15:04.000", start) + endTime, _ := time.Parse("15:04.000", end) currentTime := time.Date(0, 1, 1, current.Hour(), current.Minute(), 0, 0, time.UTC) startTimeFormatted := time.Date(0, 1, 1, startTime.Hour(), startTime.Minute(), 0, 0, time.UTC) diff --git a/pkg/orm/tool_test.go b/pkg/orm/tool_test.go index 6bdb5cd..d415bbd 100644 --- a/pkg/orm/tool_test.go +++ b/pkg/orm/tool_test.go @@ -1,6 +1,13 @@ package orm -import "testing" +import ( + "testing" + + "github.com/perfect-panel/server/internal/model/task" + + "gorm.io/driver/mysql" + "gorm.io/gorm" +) func TestParseDSN(t *testing.T) { dsn := "root:mylove520@tcp(localhost:3306)/vpnboard" @@ -16,3 +23,18 @@ func TestPing(t *testing.T) { status := Ping(dsn) t.Log(status) } + +func TestMysql(t *testing.T) { + db, err := gorm.Open(mysql.New(mysql.Config{ + DSN: "root:mylove520@tcp(localhost:3306)/vpnboard", + })) + if err != nil { + t.Fatalf("Failed to connect to MySQL: %v", err) + } + err = db.Migrator().AutoMigrate(&task.Task{}) + if err != nil { + t.Fatalf("Failed to auto migrate: %v", err) + return + } + t.Log("MySQL connection and migration successful") +} diff --git a/pkg/payment/epay/epay.go b/pkg/payment/epay/epay.go index ae1315e..8933d9f 100644 --- a/pkg/payment/epay/epay.go +++ b/pkg/payment/epay/epay.go @@ -14,9 +14,10 @@ import ( ) type Client struct { - Pid string - Url string - Key string + Pid string + Url string + Key string + Type string } type Order struct { @@ -37,11 +38,12 @@ type queryOrderStatusResponse struct { Status int `json:"status"` } -func NewClient(pid, url, key string) *Client { +func NewClient(pid, url, key string, Type string) *Client { return &Client{ - Pid: pid, - Url: url, - Key: key, + Pid: pid, + Url: url, + Key: key, + Type: Type, } } @@ -53,6 +55,7 @@ func (c *Client) CreatePayUrl(order Order) string { params.Set("notify_url", order.NotifyUrl) params.Set("out_trade_no", order.OrderNo) params.Set("pid", c.Pid) + params.Set("type", c.Type) params.Set("return_url", order.ReturnUrl) // Generate the sign using the CreateSign function @@ -117,6 +120,7 @@ func (c *Client) structToMap(order Order) map[string]string { result["notify_url"] = order.NotifyUrl result["out_trade_no"] = order.OrderNo result["pid"] = c.Pid + result["type"] = c.Type result["return_url"] = order.ReturnUrl return result } diff --git a/pkg/payment/epay/epay_test.go b/pkg/payment/epay/epay_test.go index a3c6884..87265e6 100644 --- a/pkg/payment/epay/epay_test.go +++ b/pkg/payment/epay/epay_test.go @@ -3,7 +3,7 @@ package epay import "testing" func TestEpay(t *testing.T) { - client := NewClient("", "http://127.0.0.1", "") + client := NewClient("", "http://127.0.0.1", "", "") order := Order{ Name: "测试", OrderNo: "123456789", @@ -19,7 +19,7 @@ func TestEpay(t *testing.T) { func TestQueryOrderStatus(t *testing.T) { t.Skipf("Skip TestQueryOrderStatus test") - client := NewClient("Pid", "Url", "Key") + client := NewClient("Pid", "Url", "Key", "Type") orderNo := "123456789" status := client.QueryOrderStatus(orderNo) t.Logf("OrderNo: %s, Status: %v\n", orderNo, status) @@ -40,7 +40,7 @@ func TestVerifySign(t *testing.T) { } key := "LbTabbB580zWyhXhyyww7wwvy5u8k0wl" - c := NewClient("Pid", "Url", key) + c := NewClient("Pid", "Url", key, "Type") if c.VerifySign(params) { t.Logf("Sign verification success!") } else { diff --git a/pkg/payment/platform.go b/pkg/payment/platform.go index 2955391..7ad12ea 100644 --- a/pkg/payment/platform.go +++ b/pkg/payment/platform.go @@ -9,10 +9,12 @@ const ( AlipayF2F EPay Balance - UNSUPPORTED + CryptoSaaS + UNSUPPORTED Platform = -1 ) var platformNames = map[string]Platform{ + "CryptoSaaS": CryptoSaaS, "Stripe": Stripe, "AlipayF2F": AlipayF2F, "EPay": EPay, @@ -63,9 +65,19 @@ func GetSupportedPlatforms() []types.PlatformInfo { Platform: EPay.String(), PlatformUrl: "", PlatformFieldDescription: map[string]string{ - "pid": "PID", - "url": "URL", - "key": "Key", + "pid": "PID", + "url": "URL", + "key": "Key", + "type": "Type", + }, + }, + { + Platform: CryptoSaaS.String(), + PlatformUrl: "https://t.me/CryptoSaaSBot", + PlatformFieldDescription: map[string]string{ + "endpoint": "API Endpoint", + "account_id": "Account ID", + "secret_key": "Secret Key", }, }, } diff --git a/pkg/tool/convert.go b/pkg/tool/convert.go index fa93055..0079620 100644 --- a/pkg/tool/convert.go +++ b/pkg/tool/convert.go @@ -1,6 +1,7 @@ package tool import ( + "encoding/json" "fmt" "reflect" "strconv" @@ -26,6 +27,16 @@ func ConvertValueToString(value reflect.Value) string { default: return "" } + case reflect.Struct, reflect.Map, reflect.Slice, reflect.Array: + bytes, err := json.Marshal(value.Interface()) + if err != nil { + fmt.Println("Error marshaling struct:", err.Error()) + return "" + } + if string(bytes) == "null" { + return "" + } + return string(bytes) default: return "" } diff --git a/pkg/tool/encryption.go b/pkg/tool/encryption.go index e4f205e..f51f61a 100644 --- a/pkg/tool/encryption.go +++ b/pkg/tool/encryption.go @@ -2,12 +2,14 @@ package tool import ( "crypto/md5" + "crypto/sha256" "crypto/sha512" "encoding/hex" "fmt" "strings" "github.com/anaskhan96/go-password-encoder" + "golang.org/x/crypto/bcrypt" ) var options = &password.Options{SaltLen: 16, Iterations: 100, KeyLen: 32, HashFunction: sha512.New} @@ -32,3 +34,24 @@ func Md5Encode(str string, isUpper bool) string { } return res } + +func MultiPasswordVerify(algo, salt, password, hash string) bool { + switch algo { + case "md5": + sum := md5.Sum([]byte(password)) + return hex.EncodeToString(sum[:]) == hash + case "sha256": + sum := sha256.Sum256([]byte(password)) + return hex.EncodeToString(sum[:]) == hash + case "md5salt": + sum := md5.Sum([]byte(password + salt)) + return hex.EncodeToString(sum[:]) == hash + case "default": // PPanel's default algorithm + return VerifyPassWord(password, hash) + case "bcrypt": + // Bcrypt (corresponding to PHP's password_hash/password_verify) + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil + } + return false +} diff --git a/pkg/tool/encryption_test.go b/pkg/tool/encryption_test.go index 45e0a18..8e420cf 100644 --- a/pkg/tool/encryption_test.go +++ b/pkg/tool/encryption_test.go @@ -1,7 +1,15 @@ package tool -import "testing" +import ( + "testing" +) func TestEncodePassWord(t *testing.T) { - t.Logf("EncodePassWord: %v", EncodePassWord("")) + t.Logf("EncodePassWord: %v", EncodePassWord("password")) +} + +func TestMultiPasswordVerify(t *testing.T) { + pwd := "$2y$10$WFO17pdtohfeBILjEChoGeVxpDG.u9kVCKhjDAeEeNmCjIlj3tDRy" + status := MultiPasswordVerify("bcrypt", "", "admin1", pwd) + t.Logf("MultiPasswordVerify: %v", status) } diff --git a/pkg/tool/slice.go b/pkg/tool/slice.go index 8b84ef9..3797878 100644 --- a/pkg/tool/slice.go +++ b/pkg/tool/slice.go @@ -59,6 +59,7 @@ func Int64SliceToString(intSlice []int64) string { // string slice to string func StringSliceToString(stringSlice []string) string { + stringSlice = RemoveDuplicateElements(stringSlice...) return strings.Join(stringSlice, ",") } @@ -134,7 +135,6 @@ func RemoveStringElement(arr []string, element ...string) []string { var result []string for _, str := range arr { if !Contains(element, str) { - logger.Infof("Remove Element: %s", str) result = append(result, str) } } diff --git a/pkg/tool/string.go b/pkg/tool/string.go new file mode 100644 index 0000000..2567dc0 --- /dev/null +++ b/pkg/tool/string.go @@ -0,0 +1,38 @@ +package tool + +import ( + "crypto/sha256" + "encoding/binary" + "errors" + "math/rand" +) + +func FixedUniqueString(s string, length int, alphabet string) (string, error) { + if alphabet == "" { + alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + } + if length <= 0 { + return "", errors.New("length must be > 0") + } + if length > len(alphabet) { + return "", errors.New("length greater than available unique characters") + } + + // Generate deterministic seed from SHA256 + hash := sha256.Sum256([]byte(s)) + seed := int64(binary.LittleEndian.Uint64(hash[:8])) // 前 8 字节 + + r := rand.New(rand.NewSource(seed)) + + // Copy alphabet to mutable array + data := []rune(alphabet) + + // Deterministic shuffle (Fisher–Yates) + for i := len(data) - 1; i > 0; i-- { + j := r.Intn(i + 1) + data[i], data[j] = data[j], data[i] + } + + // Take first N characters + return string(data[:length]), nil +} diff --git a/pkg/tool/string_test.go b/pkg/tool/string_test.go new file mode 100644 index 0000000..0c44086 --- /dev/null +++ b/pkg/tool/string_test.go @@ -0,0 +1,27 @@ +package tool + +import ( + "testing" +) + +func TestFixedUniqueString(t *testing.T) { + a := "example" + b := "example1" + c := "example" + + strA1, err := FixedUniqueString(a, 8, "") + strB1, err := FixedUniqueString(b, 8, "") + strC1, err := FixedUniqueString(c, 8, "") + if err != nil { + t.Logf("Error: %v", err.Error()) + return + } + if strA1 != strC1 { + t.Errorf("Expected strA1 and strC1 to be equal, got %s and %s", strA1, strC1) + } + if strA1 == strB1 { + t.Errorf("Expected strA1 and strB1 to be different, got %s and %s", strA1, strB1) + } + t.Logf("strA1 and strB1 are not equal, strA1: %s, strB1: %s", strA1, strB1) + t.Logf("strA1 and strC1 are equal,strA1: %s, strC1: %s", strA1, strC1) +} diff --git a/pkg/tool/time.go b/pkg/tool/time.go index 65008f5..31c6883 100644 --- a/pkg/tool/time.go +++ b/pkg/tool/time.go @@ -144,3 +144,10 @@ func DayDiff(startTime, endTime time.Time) int64 { duration := endTime.Sub(startTime) return int64(duration.Hours() / 24) // 转换为整天数 } + +// HourDiff 计算两个时间点之间的小时差 +func HourDiff(startTime, endTime time.Time) int64 { + // 计算时间差 + duration := endTime.Sub(startTime) + return int64(duration.Hours()) // 返回小时数,可能包含小数部分 +} diff --git a/pkg/xerr/errCode.go b/pkg/xerr/errCode.go index 64cbd6a..7e1bfd3 100644 --- a/pkg/xerr/errCode.go +++ b/pkg/xerr/errCode.go @@ -18,15 +18,16 @@ const ( // User error const ( - UserExist uint32 = 20001 - UserNotExist uint32 = 20002 - UserPasswordError uint32 = 20003 - UserDisabled uint32 = 20004 - InsufficientBalance uint32 = 20005 - StopRegister uint32 = 20006 - TelegramNotBound uint32 = 20007 - UserNotBindOauth uint32 = 20008 - InviteCodeError uint32 = 20009 + UserExist uint32 = 20001 + UserNotExist uint32 = 20002 + UserPasswordError uint32 = 20003 + UserDisabled uint32 = 20004 + InsufficientBalance uint32 = 20005 + StopRegister uint32 = 20006 + TelegramNotBound uint32 = 20007 + UserNotBindOauth uint32 = 20008 + InviteCodeError uint32 = 20009 + UserCommissionNotEnough uint32 = 20010 ) // Node error @@ -47,6 +48,7 @@ const ( ErrorTokenExpire uint32 = 40004 InvalidAccess uint32 = 40005 InvalidCiphertext uint32 = 40006 + SecretIsEmpty uint32 = 40007 ) //coupon error @@ -56,6 +58,7 @@ const ( CouponAlreadyUsed uint32 = 50002 // Coupon has already been used CouponNotApplicable uint32 = 50003 // Coupon does not match the order or conditions CouponInsufficientUsage uint32 = 50004 // Coupon has insufficient remaining uses + CouponExpired uint32 = 50005 // Coupon is expired ) // Subscribe diff --git a/pkg/xerr/errMsg.go b/pkg/xerr/errMsg.go index a259e54..f688854 100644 --- a/pkg/xerr/errMsg.go +++ b/pkg/xerr/errMsg.go @@ -14,6 +14,7 @@ func init() { ErrorTokenEmpty: "User token is empty", ErrorTokenInvalid: "User token is invalid", ErrorTokenExpire: "User token is expired", + SecretIsEmpty: "Secret is empty", InvalidAccess: "Invalid access", InvalidCiphertext: "Invalid ciphertext", // Database error @@ -45,6 +46,7 @@ func init() { CouponAlreadyUsed: "Coupon has already been used", CouponNotApplicable: "Coupon does not match the order or conditions", CouponInsufficientUsage: "Coupon has insufficient remaining uses", + CouponExpired: "Coupon is expired", // Subscribe SubscribeExpired: "Subscribe is expired", diff --git a/ppanel.api b/ppanel.api index ca98a2f..10c83c2 100644 --- a/ppanel.api +++ b/ppanel.api @@ -27,6 +27,8 @@ import ( "apis/admin/console.api" "apis/admin/log.api" "apis/admin/ads.api" + "apis/admin/marketing.api" + "apis/admin/application.api" "apis/public/user.api" "apis/public/subscribe.api" "apis/public/order.api" @@ -35,14 +37,5 @@ import ( "apis/public/payment.api" "apis/public/document.api" "apis/public/portal.api" - "apis/app/auth.api" - "apis/app/user.api" - "apis/app/node.api" - "apis/app/ws.api" - "apis/app/order.api" - "apis/app/announcement.api" - "apis/app/payment.api" - "apis/app/document.api" - "apis/app/subscribe.api" ) diff --git a/queue/handler/routes.go b/queue/handler/routes.go index 20089ed..2e96219 100644 --- a/queue/handler/routes.go +++ b/queue/handler/routes.go @@ -3,10 +3,10 @@ package handler import ( "github.com/hibiken/asynq" "github.com/perfect-panel/server/internal/svc" - countrylogic "github.com/perfect-panel/server/queue/logic/country" orderLogic "github.com/perfect-panel/server/queue/logic/order" smslogic "github.com/perfect-panel/server/queue/logic/sms" "github.com/perfect-panel/server/queue/logic/subscription" + "github.com/perfect-panel/server/queue/logic/task" "github.com/perfect-panel/server/queue/logic/traffic" "github.com/perfect-panel/server/queue/types" @@ -14,8 +14,6 @@ import ( ) func RegisterHandlers(mux *asynq.ServeMux, serverCtx *svc.ServiceContext) { - // get country task - mux.Handle(types.ForthwithGetCountry, countrylogic.NewGetNodeCountryLogic(serverCtx)) // Send email task mux.Handle(types.ForthwithSendEmail, emailLogic.NewSendEmailLogic(serverCtx)) // Send sms task @@ -36,4 +34,13 @@ func RegisterHandlers(mux *asynq.ServeMux, serverCtx *svc.ServiceContext) { // Schedule reset traffic mux.Handle(types.SchedulerResetTraffic, traffic.NewResetTrafficLogic(serverCtx)) + + // ScheduledBatchSendEmail + mux.Handle(types.ScheduledBatchSendEmail, emailLogic.NewBatchEmailLogic(serverCtx)) + + // ScheduledTrafficStat + mux.Handle(types.SchedulerTrafficStat, traffic.NewStatLogic(serverCtx)) + + // ForthwithQuotaTask + mux.Handle(types.ForthwithQuotaTask, task.NewQuotaTaskLogic(serverCtx)) } diff --git a/queue/logic/country/getCountryLogic.go b/queue/logic/country/getCountryLogic.go deleted file mode 100644 index 2b7ac41..0000000 --- a/queue/logic/country/getCountryLogic.go +++ /dev/null @@ -1,60 +0,0 @@ -package countrylogic - -import ( - "context" - "encoding/json" - - "github.com/perfect-panel/server/pkg/logger" - - "github.com/hibiken/asynq" - "github.com/perfect-panel/server/internal/svc" - "github.com/perfect-panel/server/pkg/ip" - "github.com/perfect-panel/server/queue/types" -) - -type GetNodeCountryLogic struct { - svcCtx *svc.ServiceContext -} - -func NewGetNodeCountryLogic(svcCtx *svc.ServiceContext) *GetNodeCountryLogic { - return &GetNodeCountryLogic{ - svcCtx: svcCtx, - } -} -func (l *GetNodeCountryLogic) ProcessTask(ctx context.Context, task *asynq.Task) error { - var payload types.GetNodeCountry - if err := json.Unmarshal(task.Payload(), &payload); err != nil { - logger.WithContext(ctx).Error("[GetNodeCountryLogic] Unmarshal payload failed", - logger.Field("error", err.Error()), - logger.Field("payload", task.Payload()), - ) - return nil - } - serverAddr := payload.ServerAddr - resp, err := ip.GetRegionByIp(serverAddr) - if err != nil { - logger.WithContext(ctx).Error("[GetNodeCountryLogic] ", logger.Field("error", err.Error()), logger.Field("serverAddr", serverAddr)) - return nil - } - - servers, err := l.svcCtx.ServerModel.FindNodeByServerAddrAndProtocol(ctx, payload.ServerAddr, payload.Protocol) - if err != nil { - logger.WithContext(ctx).Error("[GetNodeCountryLogic] FindNodeByServerAddrAnd", logger.Field("error", err.Error()), logger.Field("serverAddr", serverAddr)) - return err - } - if len(servers) == 0 { - return nil - } - for _, ser := range servers { - ser.Country = resp.Country - ser.City = resp.City - ser.Latitude = resp.Latitude - ser.Longitude = resp.Longitude - err := l.svcCtx.ServerModel.Update(ctx, ser) - if err != nil { - logger.WithContext(ctx).Error("[GetNodeCountryLogic] ", logger.Field("error", err.Error()), logger.Field("id", ser.Id)) - } - } - logger.WithContext(ctx).Info("[GetNodeCountryLogic] ", logger.Field("country", resp.Country), logger.Field("city", resp.Country)) - return nil -} diff --git a/queue/logic/email/batchEmailLogic.go b/queue/logic/email/batchEmailLogic.go new file mode 100644 index 0000000..2aa8123 --- /dev/null +++ b/queue/logic/email/batchEmailLogic.go @@ -0,0 +1,78 @@ +package emailLogic + +import ( + "context" + "strconv" + + "github.com/hibiken/asynq" + taskModel "github.com/perfect-panel/server/internal/model/task" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/email" + "github.com/perfect-panel/server/pkg/logger" +) + +type BatchEmailLogic struct { + svcCtx *svc.ServiceContext +} + +type ErrorInfo struct { + Error string `json:"error"` + Email string `json:"email"` + Time int64 `json:"time"` +} + +func NewBatchEmailLogic(svcCtx *svc.ServiceContext) *BatchEmailLogic { + return &BatchEmailLogic{ + svcCtx: svcCtx, + } +} + +func (l *BatchEmailLogic) ProcessTask(ctx context.Context, task *asynq.Task) error { + // 解析任务负载 + payload := task.Payload() + if len(payload) == 0 { + logger.Error("[BatchEmailLogic] ProcessTask failed: empty payload") + return asynq.SkipRetry + } + // 转换获取任务id + taskID, err := strconv.ParseInt(string(payload), 10, 64) + if err != nil { + logger.WithContext(ctx).Error("[BatchEmailLogic] ProcessTask failed: invalid task ID", + logger.Field("error", err.Error()), + logger.Field("payload", string(payload)), + ) + return asynq.SkipRetry + } + tx := l.svcCtx.DB.WithContext(ctx) + var taskInfo taskModel.Task + if err = tx.Model(&taskModel.Task{}).Where("id = ?", taskID).First(&taskInfo).Error; err != nil { + logger.WithContext(ctx).Error("[BatchEmailLogic] ProcessTask failed", + logger.Field("error", err.Error()), + logger.Field("taskID", taskID), + ) + return asynq.SkipRetry + } + + if taskInfo.Status != 0 { + logger.WithContext(ctx).Info("[BatchEmailLogic] ProcessTask skipped: task already processed", + logger.Field("taskID", taskID), + logger.Field("status", taskInfo.Status), + ) + return nil + } + + sender, err := email.NewSender(l.svcCtx.Config.Email.Platform, l.svcCtx.Config.Email.PlatformConfig, l.svcCtx.Config.Site.SiteName) + if err != nil { + logger.WithContext(ctx).Error("[BatchEmailLogic] NewSender failed", logger.Field("error", err.Error())) + return nil + } + manager := email.NewWorkerManager(l.svcCtx.DB, sender) + if manager == nil { + logger.WithContext(ctx).Error("[BatchEmailLogic] ProcessTask failed: worker manager is nil") + return asynq.SkipRetry + } + + // 添加或获取 Worker 实例 + manager.AddWorker(taskID) + return nil +} diff --git a/queue/logic/email/sendEmailLogic.go b/queue/logic/email/sendEmailLogic.go index 26ee1c0..7a56350 100644 --- a/queue/logic/email/sendEmailLogic.go +++ b/queue/logic/email/sendEmailLogic.go @@ -1,8 +1,11 @@ package emailLogic import ( + "bytes" "context" "encoding/json" + "text/template" + "time" "github.com/perfect-panel/server/pkg/logger" @@ -31,8 +34,7 @@ func (l *SendEmailLogic) ProcessTask(ctx context.Context, task *asynq.Task) erro ) return nil } - messageLog := log.MessageLog{ - Type: log.Email.String(), + messageLog := log.Message{ Platform: l.svcCtx.Config.Email.Platform, To: payload.Email, Subject: payload.Subject, @@ -43,18 +45,111 @@ func (l *SendEmailLogic) ProcessTask(ctx context.Context, task *asynq.Task) erro logger.WithContext(ctx).Error("[SendEmailLogic] NewSender failed", logger.Field("error", err.Error())) return nil } - err = sender.Send([]string{payload.Email}, payload.Subject, payload.Content) + var content string + switch payload.Type { + case types.EmailTypeVerify: + tpl, _ := template.New("verify").Parse(l.svcCtx.Config.Email.VerifyEmailTemplate) + var result bytes.Buffer + + payload.Content["Type"] = uint8(payload.Content["Type"].(float64)) + + err = tpl.Execute(&result, payload.Content) + if err != nil { + logger.WithContext(ctx).Error("[SendEmailLogic] Execute template failed", + logger.Field("error", err.Error()), + logger.Field("data", payload.Content), + ) + return nil + } + content = result.String() + case types.EmailTypeMaintenance: + tpl, _ := template.New("maintenance").Parse(l.svcCtx.Config.Email.MaintenanceEmailTemplate) + var result bytes.Buffer + err = tpl.Execute(&result, payload.Content) + if err != nil { + logger.WithContext(ctx).Error("[SendEmailLogic] Execute template failed", + logger.Field("error", err.Error()), + logger.Field("template", l.svcCtx.Config.Email.MaintenanceEmailTemplate), + logger.Field("data", payload.Content), + ) + return nil + } + content = result.String() + case types.EmailTypeExpiration: + tpl, _ := template.New("expiration").Parse(l.svcCtx.Config.Email.ExpirationEmailTemplate) + var result bytes.Buffer + err = tpl.Execute(&result, payload.Content) + if err != nil { + logger.WithContext(ctx).Error("[SendEmailLogic] Execute template failed", + logger.Field("error", err.Error()), + logger.Field("template", l.svcCtx.Config.Email.ExpirationEmailTemplate), + logger.Field("data", payload.Content), + ) + return nil + } + content = result.String() + case types.EmailTypeTrafficExceed: + tpl, _ := template.New("traffic_exceed").Parse(l.svcCtx.Config.Email.TrafficExceedEmailTemplate) + var result bytes.Buffer + err = tpl.Execute(&result, payload.Content) + if err != nil { + logger.WithContext(ctx).Error("[SendEmailLogic] Execute template failed", + logger.Field("error", err.Error()), + logger.Field("template", l.svcCtx.Config.Email.TrafficExceedEmailTemplate), + logger.Field("data", payload.Content), + ) + return nil + } + content = result.String() + case types.EmailTypeCustom: + if payload.Content == nil { + logger.WithContext(ctx).Error("[SendEmailLogic] Custom email content is empty", + logger.Field("payload", payload), + ) + return nil + } + if tpl, ok := payload.Content["content"].(string); !ok { + logger.WithContext(ctx).Error("[SendEmailLogic] Custom email content is not a string", + logger.Field("payload", payload), + ) + return nil + } else { + content = tpl + } + default: + logger.WithContext(ctx).Error("[SendEmailLogic] Unsupported email type", + logger.Field("type", payload.Type), + logger.Field("payload", payload), + ) + return nil + } + + err = sender.Send([]string{payload.Email}, payload.Subject, content) if err != nil { logger.WithContext(ctx).Error("[SendEmailLogic] Send email failed", logger.Field("error", err.Error())) return nil } messageLog.Status = 1 - if err = l.svcCtx.LogModel.InsertMessageLog(ctx, &messageLog); err != nil { - logger.WithContext(ctx).Error("[SendEmailLogic] InsertMessageLog failed", + emailLog, err := messageLog.Marshal() + if err != nil { + logger.WithContext(ctx).Error("[SendEmailLogic] Marshal message log failed", logger.Field("error", err.Error()), logger.Field("messageLog", messageLog), ) + return nil + } + + if err = l.svcCtx.LogModel.Insert(ctx, &log.SystemLog{ + Type: log.TypeEmailMessage.Uint8(), + Date: time.Now().Format("2006-01-02"), + ObjectID: 0, + Content: string(emailLog), + }); err != nil { + logger.WithContext(ctx).Error("[SendEmailLogic] Insert email log failed", + logger.Field("error", err.Error()), + logger.Field("emailLog", string(emailLog)), + ) + return nil } - logger.WithContext(ctx).Info("[SendEmailLogic] Send email", logger.Field("email", payload.Email), logger.Field("content", payload.Content)) return nil } diff --git a/queue/logic/order/activateOrderLogic.go b/queue/logic/order/activateOrderLogic.go index b4e0225..55dc284 100644 --- a/queue/logic/order/activateOrderLogic.go +++ b/queue/logic/order/activateOrderLogic.go @@ -9,13 +9,13 @@ import ( "strconv" "time" + "github.com/perfect-panel/server/internal/model/log" "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/subscribe" @@ -44,11 +44,6 @@ const ( OrderStatusFinished = 5 // Order successfully completed ) -// Commission type constants define the types of commission transactions -const ( - CommissionTypeRecharge = 1 // Commission from balance recharge -) - // Predefined error variables for common error conditions var ( ErrInvalidOrderStatus = fmt.Errorf("invalid order status") @@ -81,7 +76,7 @@ func (l *ActivateOrderLogic) ProcessTask(ctx context.Context, task *asynq.Task) return nil // Log and continue } - if err := l.processOrderByType(ctx, orderInfo); err != nil { + if err = l.processOrderByType(ctx, orderInfo); err != nil { logger.WithContext(ctx).Error("[ActivateOrderLogic] Process task failed", logger.Field("error", err.Error())) return nil } @@ -90,7 +85,7 @@ func (l *ActivateOrderLogic) ProcessTask(ctx context.Context, task *asynq.Task) return nil } -// parsePayload unmarshals the task payload into a structured format +// parsePayload unMarshals the task payload into a structured format func (l *ActivateOrderLogic) parsePayload(ctx context.Context, payload []byte) (*types.ForthwithActivateOrderPayload, error) { var p types.ForthwithActivateOrderPayload if err := json.Unmarshal(payload, &p); err != nil { @@ -185,7 +180,7 @@ func (l *ActivateOrderLogic) NewPurchase(ctx context.Context, orderInfo *order.O } // Handle commission in separate goroutine to avoid blocking - go l.handleCommission(context.Background(), userInfo, orderInfo, true) + go l.handleCommission(context.Background(), userInfo, orderInfo) // Clear cache l.clearServerCache(ctx, sub) @@ -228,6 +223,7 @@ func (l *ActivateOrderLogic) createGuestUser(ctx context.Context, orderInfo *ord userInfo := &user.User{ Password: tool.EncodePassWord(tempOrder.Password), + Algo: "default", AuthMethods: []user.AuthMethods{ { AuthType: tempOrder.AuthType, @@ -280,8 +276,12 @@ func (l *ActivateOrderLogic) getTempOrderInfo(ctx context.Context, orderNo strin } var tempOrder constant.TemporaryOrderInfo - if err = json.Unmarshal([]byte(data), &tempOrder); err != nil { - logger.WithContext(ctx).Error("Unmarshal temp order failed", logger.Field("error", err.Error())) + if err = tempOrder.Unmarshal([]byte(data)); err != nil { + logger.WithContext(ctx).Error("Unmarshal temp order cache failed", + logger.Field("error", err.Error()), + logger.Field("cache_key", cacheKey), + logger.Field("data", data), + ) return nil, err } @@ -352,8 +352,8 @@ func (l *ActivateOrderLogic) createUserSubscription(ctx context.Context, orderIn // handleCommission processes referral commission for the referrer if applicable. // This runs asynchronously to avoid blocking the main order processing flow. -func (l *ActivateOrderLogic) handleCommission(ctx context.Context, userInfo *user.User, orderInfo *order.Order, isNewPurchase bool) { - if !l.shouldProcessCommission(userInfo, orderInfo, isNewPurchase) { +func (l *ActivateOrderLogic) handleCommission(ctx context.Context, userInfo *user.User, orderInfo *order.Order) { + if !l.shouldProcessCommission(userInfo, orderInfo.IsNew) { return } @@ -366,21 +366,45 @@ func (l *ActivateOrderLogic) handleCommission(ctx context.Context, userInfo *use return } - amount := l.calculateCommission(orderInfo.Amount, isNewPurchase && orderInfo.IsNew, orderInfo.Quantity) + var referralPercentage uint8 + if referer.ReferralPercentage != 0 { + referralPercentage = referer.ReferralPercentage + } else { + referralPercentage = uint8(l.svc.Config.Invite.ReferralPercentage) + } + + // Order commission calculation: (Order Amount - Order Fee) * Referral Percentage + amount := l.calculateCommission(orderInfo.Amount-orderInfo.FeeAmount, referralPercentage) // Use transaction for commission updates err = l.svc.DB.Transaction(func(tx *gorm.DB) error { referer.Commission += amount - if err := l.svc.UserModel.Update(ctx, referer, tx); err != nil { + if err = l.svc.UserModel.Update(ctx, referer, tx); err != nil { return err } - commissionLog := &user.CommissionLog{ - UserId: referer.Id, - OrderNo: orderInfo.OrderNo, - Amount: amount, + var commissionType uint16 + switch orderInfo.Type { + case OrderTypeSubscribe: + commissionType = log.CommissionTypePurchase + case OrderTypeRenewal: + commissionType = log.CommissionTypeRenewal } - return l.svc.UserModel.InsertCommissionLog(ctx, commissionLog, tx) + + commissionLog := &log.Commission{ + Type: commissionType, + Amount: amount, + OrderNo: orderInfo.OrderNo, + Timestamp: orderInfo.CreatedAt.UnixMilli(), + } + + content, _ := commissionLog.Marshal() + return tx.Model(&log.SystemLog{}).Create(&log.SystemLog{ + Type: log.TypeCommission.Uint8(), + Date: time.Now().Format("2006-01-02"), + ObjectID: referer.Id, + Content: string(content), + }).Error }) if err != nil { @@ -389,7 +413,7 @@ func (l *ActivateOrderLogic) handleCommission(ctx context.Context, userInfo *use } // Update cache - if err := l.svc.UserModel.UpdateUserCache(ctx, referer); err != nil { + if err = l.svc.UserModel.UpdateUserCache(ctx, referer); err != nil { logger.WithContext(ctx).Error("Update referer cache failed", logger.Field("error", err.Error()), logger.Field("user_id", referer.Id), @@ -399,61 +423,53 @@ func (l *ActivateOrderLogic) handleCommission(ctx context.Context, userInfo *use // shouldProcessCommission determines if commission should be processed based on // referrer existence, commission settings, and order type -func (l *ActivateOrderLogic) shouldProcessCommission(userInfo *user.User, orderInfo *order.Order, isNewPurchase bool) bool { - return userInfo.RefererId != 0 && - (l.svc.Config.Invite.FirstPurchasePercentage != 0 || - l.svc.Config.Invite.FirstYearlyPurchasePercentage != 0 || - l.svc.Config.Invite.NonFirstPurchasePercentage != 0) +func (l *ActivateOrderLogic) shouldProcessCommission(userInfo *user.User, isFirstPurchase bool) bool { + if userInfo == nil || userInfo.RefererId == 0 { + return false + } + + referer, err := l.svc.UserModel.FindOne(context.Background(), userInfo.RefererId) + if err != nil { + logger.Errorw("Find referer failed", + logger.Field("error", err.Error()), + logger.Field("referer_id", userInfo.RefererId)) + return false + } + if referer == nil { + return false + } + + // use referer's custom settings if set + if referer.ReferralPercentage > 0 { + if referer.OnlyFirstPurchase != nil && *referer.OnlyFirstPurchase && !isFirstPurchase { + return false + } + return true + } + + // use global settings + if l.svc.Config.Invite.ReferralPercentage == 0 { + return false + } + if l.svc.Config.Invite.OnlyFirstPurchase && !isFirstPurchase { + return false + } + + return true } -// calculateCommission computes the commission amount based on order price and purchase type -func (l *ActivateOrderLogic) calculateCommission(price int64, isFirstPurchase bool, quantity int64) int64 { - var percentage int64 - if isFirstPurchase { - // 判断是否为年付(12个月) - if quantity == 12 { - percentage = l.svc.Config.Invite.FirstYearlyPurchasePercentage - } else { - percentage = l.svc.Config.Invite.FirstPurchasePercentage - } - } else { - percentage = l.svc.Config.Invite.NonFirstPurchasePercentage - } +// calculateCommission computes the commission amount based on order price and referral percentage +func (l *ActivateOrderLogic) calculateCommission(price int64, percentage uint8) int64 { return int64(float64(price) * (float64(percentage) / 100)) } // clearServerCache clears user list cache for all servers associated with the subscription func (l *ActivateOrderLogic) clearServerCache(ctx context.Context, sub *subscribe.Subscribe) { - serverIds := tool.StringToInt64Slice(sub.Server) - groupServerIds := l.getServerIdsByGroups(ctx, sub.ServerGroup) - allServerIds := append(serverIds, groupServerIds...) - - for _, id := range allServerIds { - cacheKey := fmt.Sprintf("%s%d", config.ServerUserListCacheKey, id) - if err := l.svc.Redis.Del(ctx, cacheKey).Err(); err != nil { - logger.WithContext(ctx).Error("Del server user list cache failed", - logger.Field("error", err.Error()), - logger.Field("cache_key", cacheKey), - ) - } + if err := l.svc.SubscribeModel.ClearCache(ctx, sub.Id); err != nil { + logger.WithContext(ctx).Error("[Order Queue] Clear subscribe cache failed", logger.Field("error", err.Error())) } } -// getServerIdsByGroups retrieves server IDs from server groups -func (l *ActivateOrderLogic) getServerIdsByGroups(ctx context.Context, serverGroup string) []int64 { - data, err := l.svc.ServerModel.FindServerListByGroupIds(ctx, tool.StringToInt64Slice(serverGroup)) - if err != nil { - logger.WithContext(ctx).Error("Find server list failed", logger.Field("error", err.Error())) - return nil - } - - serverIds := make([]int64, len(data)) - for i, item := range data { - serverIds[i] = item.Id - } - return serverIds -} - // Renewal handles subscription renewal including subscription extension, // traffic reset (if configured), commission processing, and notifications func (l *ActivateOrderLogic) Renewal(ctx context.Context, orderInfo *order.Order) error { @@ -472,12 +488,25 @@ func (l *ActivateOrderLogic) Renewal(ctx context.Context, orderInfo *order.Order return err } - if err := l.updateSubscriptionForRenewal(ctx, userSub, sub, orderInfo); err != nil { + if err = l.updateSubscriptionForRenewal(ctx, userSub, sub, orderInfo); err != nil { return err } + // Clear user subscription cache + err = l.svc.UserModel.ClearSubscribeCache(ctx, userSub) + if err != nil { + logger.WithContext(ctx).Error("Clear user subscribe cache failed", + logger.Field("error", err.Error()), + logger.Field("subscribe_id", userSub.Id), + logger.Field("user_id", userInfo.Id), + ) + } + + // Clear cache + l.clearServerCache(ctx, sub) + // Handle commission - go l.handleCommission(context.Background(), userInfo, orderInfo, false) + go l.handleCommission(context.Background(), userInfo, orderInfo) // Send notifications l.sendNotifications(ctx, orderInfo, userInfo, sub, userSub, telegram.RenewalNotify) @@ -502,14 +531,22 @@ func (l *ActivateOrderLogic) updateSubscriptionForRenewal(ctx context.Context, u if userSub.ExpireTime.Before(now) { userSub.ExpireTime = now } + today := time.Now().Day() + resetDay := userSub.ExpireTime.Day() // Reset traffic if enabled - if sub.RenewalReset != nil && *sub.RenewalReset { + if (sub.RenewalReset != nil && *sub.RenewalReset) || today == resetDay { userSub.Download = 0 userSub.Upload = 0 } if userSub.FinishedAt != nil { + if userSub.FinishedAt.Before(now) && today > resetDay { + // reset user traffic if finished at is before now + userSub.Download = 0 + userSub.Upload = 0 + } + userSub.FinishedAt = nil } @@ -551,6 +588,37 @@ func (l *ActivateOrderLogic) ResetTraffic(ctx context.Context, orderInfo *order. return err } + // Clear user subscription cache + err = l.svc.UserModel.ClearSubscribeCache(ctx, userSub) + if err != nil { + logger.WithContext(ctx).Error("Clear user subscribe cache failed", + logger.Field("error", err.Error()), + logger.Field("subscribe_id", userSub.Id), + logger.Field("user_id", userInfo.Id), + ) + } + + // Clear cache + l.clearServerCache(ctx, sub) + + // insert reset traffic log + resetLog := &log.ResetSubscribe{ + Type: log.ResetSubscribeTypePaid, + UserId: userInfo.Id, + OrderNo: orderInfo.OrderNo, + Timestamp: time.Now().UnixMilli(), + } + + content, _ := resetLog.Marshal() + if err = l.svc.LogModel.Insert(ctx, &log.SystemLog{ + Type: log.TypeResetSubscribe.Uint8(), + Date: time.Now().Format(time.DateOnly), + ObjectID: userSub.Id, + Content: string(content), + }); err != nil { + logger.WithContext(ctx).Error("[Order Queue]Insert reset subscribe log failed", logger.Field("error", err.Error())) + } + // Send notifications l.sendNotifications(ctx, orderInfo, userInfo, sub, userSub, telegram.ResetTrafficNotify) @@ -568,22 +636,35 @@ func (l *ActivateOrderLogic) Recharge(ctx context.Context, orderInfo *order.Orde // Update balance in transaction err = l.svc.DB.Transaction(func(tx *gorm.DB) error { userInfo.Balance += orderInfo.Price - if err := l.svc.UserModel.Update(ctx, userInfo, tx); err != nil { + if err = l.svc.UserModel.Update(ctx, userInfo, tx); err != nil { return err } - balanceLog := &user.BalanceLog{ - UserId: orderInfo.UserId, - Amount: orderInfo.Price, - Type: CommissionTypeRecharge, - OrderId: orderInfo.Id, - Balance: userInfo.Balance, + balanceLog := &log.Balance{ + Amount: orderInfo.Price, + Type: log.BalanceTypeRecharge, + OrderNo: orderInfo.OrderNo, + Balance: userInfo.Balance, + Timestamp: time.Now().UnixMilli(), } - return l.svc.UserModel.InsertBalanceLog(ctx, balanceLog, tx) + content, _ := balanceLog.Marshal() + + return tx.Model(&log.SystemLog{}).Create(&log.SystemLog{ + Type: log.TypeBalance.Uint8(), + Date: time.Now().Format("2006-01-02"), + ObjectID: userInfo.Id, + Content: string(content), + }).Error }) if err != nil { - logger.WithContext(ctx).Error("Database transaction failed", logger.Field("error", err.Error())) + logger.WithContext(ctx).Error("[Recharge] Database transaction failed", logger.Field("error", err.Error())) + return err + } + + // clear user cache + if err = l.svc.UserModel.UpdateUserCache(ctx, userInfo); err != nil { + logger.WithContext(ctx).Error("[Recharge] Update user cache failed", logger.Field("error", err.Error())) return err } diff --git a/queue/logic/sms/sendSmsLogic.go b/queue/logic/sms/sendSmsLogic.go index 196309b..76cef6d 100644 --- a/queue/logic/sms/sendSmsLogic.go +++ b/queue/logic/sms/sendSmsLogic.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "time" "github.com/perfect-panel/server/pkg/logger" @@ -43,17 +44,16 @@ func (l *SendSmsLogic) ProcessTask(ctx context.Context, task *asynq.Task) error logger.WithContext(ctx).Error("[SendSmsLogic] New send sms client failed", logger.Field("error", err.Error()), logger.Field("payload", payload)) return err } - createSms := &log.MessageLog{ - Type: log.Mobile.String(), + createSms := &log.Message{ Platform: l.svcCtx.Config.Mobile.Platform, To: fmt.Sprintf("+%s%s", payload.TelephoneArea, payload.Telephone), Subject: constant.ParseVerifyType(payload.Type).String(), - Content: "", + Content: map[string]interface{}{ + "content": client.GetSendCodeContent(payload.Content), + }, } err = client.SendCode(payload.TelephoneArea, payload.Telephone, payload.Content) - createSms.Content = client.GetSendCodeContent(payload.Content) - if err != nil { logger.WithContext(ctx).Error("[SendSmsLogic] Send sms failed", logger.Field("error", err.Error()), logger.Field("payload", payload)) if l.svcCtx.Config.Model != constant.DevMode { @@ -64,7 +64,14 @@ func (l *SendSmsLogic) ProcessTask(ctx context.Context, task *asynq.Task) error } createSms.Status = 1 logger.WithContext(ctx).Info("[SendSmsLogic] Send sms", logger.Field("telephone", payload.Telephone), logger.Field("content", createSms.Content)) - err = l.svcCtx.LogModel.InsertMessageLog(ctx, createSms) + + content, _ := createSms.Marshal() + err = l.svcCtx.LogModel.Insert(ctx, &log.SystemLog{ + Type: log.TypeMobileMessage.Uint8(), + Date: time.Now().Format("2006-01-02"), + ObjectID: 0, + Content: string(content), + }) if err != nil { logger.WithContext(ctx).Error("[SendSmsLogic] Send sms failed", logger.Field("error", err.Error()), logger.Field("payload", payload)) return nil diff --git a/queue/logic/subscription/checkSubscriptionLogic.go b/queue/logic/subscription/checkSubscriptionLogic.go index ef77130..81b86e7 100644 --- a/queue/logic/subscription/checkSubscriptionLogic.go +++ b/queue/logic/subscription/checkSubscriptionLogic.go @@ -1,10 +1,8 @@ package subscription import ( - "bytes" "context" "encoding/json" - "text/template" "time" queue "github.com/perfect-panel/server/queue/types" @@ -32,7 +30,7 @@ func (l *CheckSubscriptionLogic) ProcessTask(ctx context.Context, _ *asynq.Task) // Check subscription traffic err := l.svc.UserModel.Transaction(ctx, func(db *gorm.DB) error { var list []*user.Subscribe - err := db.Model(&user.Subscribe{}).Where("upload + download >= traffic AND status = 1 AND traffic > 0 ").Find(&list).Error + err := db.Model(&user.Subscribe{}).Where("upload + download >= traffic AND status IN (0, 1) AND traffic > 0 ").Find(&list).Error if err != nil { logger.Errorw("[Check Subscription Traffic] Query subscribe failed", logger.Field("error", err.Error())) return err @@ -62,7 +60,7 @@ func (l *CheckSubscriptionLogic) ProcessTask(ctx context.Context, _ *asynq.Task) return err } } - + l.clearServerCache(ctx, list...) logger.Infow("[Check Subscription Traffic] Update subscribe status", logger.Field("user_ids", ids), logger.Field("count", int64(len(ids)))) } else { @@ -77,7 +75,7 @@ func (l *CheckSubscriptionLogic) ProcessTask(ctx context.Context, _ *asynq.Task) // Check subscription expire err = l.svc.UserModel.Transaction(ctx, func(db *gorm.DB) error { var list []*user.Subscribe - err = db.Model(&user.Subscribe{}).Where("`status` = 1 AND `expire_time` < ? AND `expire_time` != ? and `finished_at` IS NULL", time.Now(), time.UnixMilli(0)).Find(&list).Error + err = db.Model(&user.Subscribe{}).Where("`status` IN (0, 1) AND `expire_time` < ? AND `expire_time` != ? and `finished_at` IS NULL", time.Now(), time.UnixMilli(0)).Find(&list).Error if err != nil { logger.Error("[Check Subscription] Find subscribe failed", logger.Field("error", err.Error())) return err @@ -104,6 +102,8 @@ func (l *CheckSubscriptionLogic) ProcessTask(ctx context.Context, _ *asynq.Task) logger.Errorw("[Check Subscription Traffic] Clear subscribe cache failed", logger.Field("error", err.Error())) return err } + l.clearServerCache(ctx, list...) + logger.Info("[Check Subscription Expire] Update subscribe status", logger.Field("user_ids", ids), logger.Field("count", int64(len(ids)))) } else { logger.Info("[Check Subscription Expire] No subscribe need to update") @@ -129,24 +129,14 @@ func (l *CheckSubscriptionLogic) sendExpiredNotify(ctx context.Context, subs []i continue } var taskPayload queue.SendEmailPayload + taskPayload.Type = queue.EmailTypeExpiration taskPayload.Email = method.AuthIdentifier taskPayload.Subject = "Subscription Expired" - tpl, err := template.New("Expired").Parse(l.svc.Config.Email.ExpirationEmailTemplate) - if err != nil { - logger.Errorw("[CheckSubscription] Parse template failed", logger.Field("error", err.Error())) - continue - } - var result bytes.Buffer - err = tpl.Execute(&result, map[string]interface{}{ + taskPayload.Content = map[string]interface{}{ "SiteLogo": l.svc.Config.Site.SiteLogo, "SiteName": l.svc.Config.Site.SiteName, "ExpireDate": sub.ExpireTime.Format("2006-01-02 15:04:05"), - }) - if err != nil { - logger.Errorw("[CheckSubscription] Execute template failed", logger.Field("error", err.Error())) - continue } - taskPayload.Content = result.String() payloadBuy, err := json.Marshal(taskPayload) if err != nil { logger.Errorw("[CheckSubscription] Marshal payload failed", logger.Field("error", err.Error())) @@ -179,23 +169,13 @@ func (l *CheckSubscriptionLogic) sendTrafficNotify(ctx context.Context, subs []i continue } var taskPayload queue.SendEmailPayload + taskPayload.Type = queue.EmailTypeTrafficExceed taskPayload.Email = method.AuthIdentifier taskPayload.Subject = "Subscription Traffic Exceed" - tpl, err := template.New("Traffic").Parse(l.svc.Config.Email.TrafficExceedEmailTemplate) - if err != nil { - logger.Errorw("[CheckSubscription] Parse template failed", logger.Field("error", err.Error())) - continue - } - var result bytes.Buffer - err = tpl.Execute(&result, map[string]interface{}{ + taskPayload.Content = map[string]interface{}{ "SiteLogo": l.svc.Config.Site.SiteLogo, "SiteName": l.svc.Config.Site.SiteName, - }) - if err != nil { - logger.Errorw("[CheckSubscription] Execute template failed", logger.Field("error", err.Error())) - continue } - taskPayload.Content = result.String() payloadBuy, err := json.Marshal(taskPayload) if err != nil { logger.Errorw("[CheckSubscription] Marshal payload failed", logger.Field("error", err.Error())) @@ -214,3 +194,18 @@ func (l *CheckSubscriptionLogic) sendTrafficNotify(ctx context.Context, subs []i } return nil } + +func (l *CheckSubscriptionLogic) clearServerCache(ctx context.Context, userSubs ...*user.Subscribe) { + subs := make(map[int64]bool) + for _, sub := range userSubs { + if _, ok := subs[sub.SubscribeId]; !ok { + subs[sub.SubscribeId] = true + } + } + + for sub, _ := range subs { + if err := l.svc.SubscribeModel.ClearCache(ctx, sub); err != nil { + logger.Errorw("[CheckSubscription] ClearCache failed", logger.Field("error", err.Error()), logger.Field("subscribe_id", sub)) + } + } +} diff --git a/queue/logic/task/quotaLogic.go b/queue/logic/task/quotaLogic.go new file mode 100644 index 0000000..c2b1464 --- /dev/null +++ b/queue/logic/task/quotaLogic.go @@ -0,0 +1,407 @@ +package task + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + "time" + + "github.com/hibiken/asynq" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/model/task" + "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/pkg/tool" + "gorm.io/gorm" +) + +const ( + UnitTimeNoLimit = "NoLimit" // Unlimited time subscription + UnitTimeYear = "Year" // Annual subscription + UnitTimeMonth = "Month" // Monthly subscription + UnitTimeDay = "Day" // Daily subscription + UnitTimeHour = "Hour" // Hourly subscription + UnitTimeMinute = "Minute" // Per-minute subscription + +) + +type QuotaTaskLogic struct { + svcCtx *svc.ServiceContext +} + +type ErrorInfo struct { + UserSubscribeId int64 `json:"user_subscribe_id"` + Error string `json:"error"` +} + +func NewQuotaTaskLogic(svcCtx *svc.ServiceContext) *QuotaTaskLogic { + return &QuotaTaskLogic{ + svcCtx: svcCtx, + } +} + +func (l *QuotaTaskLogic) ProcessTask(ctx context.Context, t *asynq.Task) error { + taskID, err := l.parseTaskID(ctx, t.Payload()) + if err != nil { + return err + } + + taskInfo, err := l.getTaskInfo(ctx, taskID) + if err != nil { + return err + } + + if taskInfo.Status != 0 { + logger.WithContext(ctx).Info("[QuotaTaskLogic.ProcessTask] task already processed", + logger.Field("taskID", taskID), + logger.Field("status", taskInfo.Status), + ) + return nil + } + + scope, content, err := l.parseTaskData(ctx, taskInfo) + if err != nil { + return err + } + + subscribes, err := l.getSubscribes(ctx, scope.Objects) + if err != nil { + return err + } + if err = l.processSubscribes(ctx, subscribes, content, taskInfo); err != nil { + return err + } + // 清理用户缓存(仅在有赠送金时清理) + if content.GiftValue != 0 { + var userIds []int64 + for _, sub := range subscribes { + userIds = append(userIds, sub.UserId) + } + userIds = tool.RemoveDuplicateElements(userIds...) + var users []*user.User + if err = l.svcCtx.DB.WithContext(ctx).Model(&user.User{}).Where("id IN ?", userIds).Find(&users).Error; err != nil { + logger.WithContext(ctx).Error("[QuotaTaskLogic.ProcessTask] find users error", + logger.Field("error", err.Error()), + logger.Field("userIDs", userIds)) + } + err = l.svcCtx.UserModel.ClearUserCache(ctx, users...) + if err != nil { + logger.WithContext(ctx).Error("[QuotaTaskLogic.ProcessTask] clear user cache error", + logger.Field("error", err.Error()), + logger.Field("userIDs", userIds)) + } + } + + // 清理用户订阅缓存 + err = l.svcCtx.UserModel.ClearSubscribeCache(ctx, subscribes...) + if err != nil { + logger.WithContext(ctx).Error("[QuotaTaskLogic.ProcessTask] clear subscribe cache error", + logger.Field("error", err.Error())) + } + + return nil +} + +func (l *QuotaTaskLogic) parseTaskID(ctx context.Context, payload []byte) (int64, error) { + if len(payload) == 0 { + logger.WithContext(ctx).Error("[QuotaTaskLogic.parseTaskID] empty payload") + return 0, asynq.SkipRetry + } + + taskID, err := strconv.ParseInt(string(payload), 10, 64) + if err != nil { + logger.WithContext(ctx).Error("[QuotaTaskLogic.parseTaskID] invalid task ID", + logger.Field("error", err.Error()), + logger.Field("payload", string(payload)), + ) + return 0, asynq.SkipRetry + } + return taskID, nil +} + +func (l *QuotaTaskLogic) getTaskInfo(ctx context.Context, taskID int64) (*task.Task, error) { + var taskInfo *task.Task + if err := l.svcCtx.DB.WithContext(ctx).Model(&task.Task{}).Where("id = ?", taskID).First(&taskInfo).Error; err != nil { + logger.WithContext(ctx).Error("[QuotaTaskLogic.getTaskInfo] find task error", + logger.Field("error", err.Error()), + logger.Field("taskID", taskID), + ) + return nil, asynq.SkipRetry + } + return taskInfo, nil +} + +func (l *QuotaTaskLogic) parseTaskData(ctx context.Context, taskInfo *task.Task) (task.QuotaScope, task.QuotaContent, error) { + var scope task.QuotaScope + if err := scope.Unmarshal([]byte(taskInfo.Scope)); err != nil { + logger.WithContext(ctx).Error("[QuotaTaskLogic.parseTaskData] unmarshal scope error", + logger.Field("error", err.Error()), + ) + return scope, task.QuotaContent{}, asynq.SkipRetry + } + + var content task.QuotaContent + if err := content.Unmarshal([]byte(taskInfo.Content)); err != nil { + logger.WithContext(ctx).Error("[QuotaTaskLogic.parseTaskData] unmarshal content error", + logger.Field("error", err.Error()), + ) + return scope, content, asynq.SkipRetry + } + return scope, content, nil +} + +func (l *QuotaTaskLogic) getSubscribes(ctx context.Context, subscriberIDs []int64) ([]*user.Subscribe, error) { + var subscribes []*user.Subscribe + if err := l.svcCtx.DB.WithContext(ctx).Model(&user.Subscribe{}).Where("id IN ?", subscriberIDs).Find(&subscribes).Error; err != nil { + logger.WithContext(ctx).Error("[QuotaTaskLogic.getSubscribes] find subscribes error", + logger.Field("error", err.Error()), + logger.Field("subscribers", subscriberIDs), + ) + return nil, asynq.SkipRetry + } + return subscribes, nil +} + +func (l *QuotaTaskLogic) processSubscribes(ctx context.Context, subscribes []*user.Subscribe, content task.QuotaContent, taskInfo *task.Task) error { + tx := l.svcCtx.DB.WithContext(ctx).Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + logger.WithContext(ctx).Error("[QuotaTaskLogic.processSubscribes] transaction panic", + logger.Field("panic", r), + ) + } + }() + + var errors []ErrorInfo + now := time.Now() + + for _, sub := range subscribes { + if err := l.processSubscription(tx, sub, content, now, &errors); err != nil { + tx.Rollback() + return err + } + } + + // 根据错误情况决定任务状态 + status := int8(2) // Completed + if len(errors) > 0 { + logger.WithContext(ctx).Error("[QuotaTaskLogic.processSubscribes] some subscriptions failed", + logger.Field("total", len(subscribes)), + logger.Field("failed", len(errors)), + ) + // 如果所有订阅都失败,标记为失败状态 + if len(errors) == len(subscribes) { + status = 3 // Failed + } + errs, err := json.Marshal(errors) + if err != nil { + logger.WithContext(ctx).Error("[QuotaTaskLogic.processSubscribes] marshal errors failed", + logger.Field("error", err.Error()), + ) + tx.Rollback() + return err + } + taskInfo.Errors = string(errs) + } + + taskInfo.Current = uint64(len(subscribes)) + taskInfo.Status = status + err := tx.Where("id = ?", taskInfo.Id).Save(taskInfo).Error + if err != nil { + logger.WithContext(ctx).Error("[QuotaTaskLogic.processSubscribes] update task status error", + logger.Field("error", err.Error()), + logger.Field("taskID", taskInfo.Id), + ) + tx.Rollback() + return err + } + + if err = tx.Commit().Error; err != nil { + logger.WithContext(ctx).Error("[QuotaTaskLogic.processSubscribes] commit transaction error", + logger.Field("error", err.Error()), + ) + return err + } + + return nil +} + +func (l *QuotaTaskLogic) processSubscription(tx *gorm.DB, sub *user.Subscribe, content task.QuotaContent, now time.Time, errors *[]ErrorInfo) error { + // 验证订阅数据 + if sub == nil { + *errors = append(*errors, ErrorInfo{ + UserSubscribeId: 0, + Error: "subscription is nil", + }) + return nil + } + + updated := false + + // 处理时间延长 - 修复逻辑:只要Days不为0就处理,不管ExpireTime是否为0 + if content.Days != 0 { + if sub.ExpireTime.Unix() == 0 || sub.ExpireTime.Before(now) { + // 如果没有过期时间或已过期,从现在开始计算 + sub.ExpireTime = now.AddDate(0, 0, int(content.Days)) + } else { + // 在原有过期时间基础上延长 + sub.ExpireTime = sub.ExpireTime.AddDate(0, 0, int(content.Days)) + } + // 如果订阅延长到未来时间,设置为激活状态 + if sub.ExpireTime.After(now) && sub.Status != 1 { + sub.Status = 1 // Active + } + updated = true + } + + // 处理流量重置 + if content.ResetTraffic { + sub.Download = 0 + sub.Upload = 0 + updated = true + if err := l.createResetTrafficLog(tx, sub.Id, sub.UserId, now); err != nil { + // 记录错误但不阻断整个任务,日志失败不影响主流程 + *errors = append(*errors, ErrorInfo{ + UserSubscribeId: sub.Id, + Error: "create reset traffic log error: " + err.Error(), + }) + } + } + + // 处理赠送金 + if content.GiftValue != 0 { + if err := l.processGift(tx, sub, content, now, errors); err != nil { + return err + } + } + + // 只有在有更新时才保存订阅信息 + if updated { + if err := tx.Where("id = ?", sub.Id).Save(sub).Error; err != nil { + *errors = append(*errors, ErrorInfo{ + UserSubscribeId: sub.Id, + Error: "update subscription error: " + err.Error(), + }) + return nil + } + } + + return nil +} + +func (l *QuotaTaskLogic) processGift(tx *gorm.DB, sub *user.Subscribe, content task.QuotaContent, now time.Time, errors *[]ErrorInfo) error { + // 验证赠送类型 + if content.GiftType != 1 && content.GiftType != 2 { + *errors = append(*errors, ErrorInfo{ + UserSubscribeId: sub.Id, + Error: fmt.Sprintf("invalid gift type: %d", content.GiftType), + }) + return nil + } + + var userInfo user.User + if err := tx.Model(&user.User{}).Where("id = ?", sub.UserId).First(&userInfo).Error; err != nil { + *errors = append(*errors, ErrorInfo{ + UserSubscribeId: sub.Id, + Error: "find user error: " + err.Error(), + }) + return nil + } + + var giftAmount int64 + switch content.GiftType { + case 1: + giftAmount = int64(content.GiftValue) + case 2: + // 获取订阅对应的套餐信息 + subscribeInfo, err := l.svcCtx.SubscribeModel.FindOne(context.Background(), sub.SubscribeId) + if err != nil { + *errors = append(*errors, ErrorInfo{ + UserSubscribeId: sub.Id, + Error: "find subscribe error: " + err.Error(), + }) + return nil + } + if subscribeInfo.UnitPrice > 0 { + giftAmount = int64(float64(subscribeInfo.UnitPrice) * (float64(content.GiftValue) / 100)) + } + } + + if giftAmount > 0 { + userInfo.GiftAmount += giftAmount + // 使用Update而不是Save,更精确地更新单个字段 + if err := tx.Model(&user.User{}).Where("id = ?", sub.UserId).Update("gift_amount", userInfo.GiftAmount).Error; err != nil { + *errors = append(*errors, ErrorInfo{ + UserSubscribeId: sub.Id, + Error: "update user gift amount error: " + err.Error(), + }) + return nil + } + + if err := l.createGiftLog(tx, sub.Id, userInfo.Id, giftAmount, userInfo.GiftAmount, now); err != nil { + *errors = append(*errors, ErrorInfo{ + UserSubscribeId: sub.Id, + Error: "create gift log error: " + err.Error(), + }) + // 回滚用户金额更新 + userInfo.GiftAmount -= giftAmount + tx.Model(&user.User{}).Where("id = ?", sub.UserId).Update("gift_amount", userInfo.GiftAmount) + return nil + } + } + + return nil +} + +func (l *QuotaTaskLogic) getStartTime(sub *user.Subscribe, now time.Time) time.Time { + if sub.StartTime.Unix() == 0 { + return now + } + return sub.StartTime +} + +func (l *QuotaTaskLogic) createGiftLog(tx *gorm.DB, subscribeId, userId, amount, balance int64, now time.Time) error { + giftLog := &log.Gift{ + Type: log.GiftTypeIncrease, + OrderNo: "", + SubscribeId: subscribeId, + Amount: amount, + Balance: balance, + Remark: "Quota task gift", + Timestamp: now.UnixMilli(), + } + + logString, err := giftLog.Marshal() + if err != nil { + return fmt.Errorf("marshal gift log error: %v", err) + } + return tx.Model(&log.SystemLog{}).Create(&log.SystemLog{ + Type: log.TypeGift.Uint8(), + Content: string(logString), + ObjectID: userId, + Date: now.Format(time.DateOnly), + }).Error +} + +func (l *QuotaTaskLogic) createResetTrafficLog(tx *gorm.DB, subscribeId, userId int64, now time.Time) error { + trafficLog := &log.ResetSubscribe{ + Type: log.ResetSubscribeTypeQuota, + UserId: userId, + OrderNo: "", + Timestamp: now.UnixMilli(), + } + + logString, err := trafficLog.Marshal() + if err != nil { + return fmt.Errorf("marshal traffic log error: %v", err) + } + return tx.Model(&log.SystemLog{}).Create(&log.SystemLog{ + Type: log.TypeResetSubscribe.Uint8(), + Content: string(logString), + ObjectID: subscribeId, + Date: now.Format(time.DateOnly), + }).Error +} diff --git a/queue/logic/task/rateLogic.go b/queue/logic/task/rateLogic.go new file mode 100644 index 0000000..2e33fae --- /dev/null +++ b/queue/logic/task/rateLogic.go @@ -0,0 +1,52 @@ +package task + +import ( + "context" + + "github.com/hibiken/asynq" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/exchangeRate" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" +) + +type RateLogic struct { + svcCtx *svc.ServiceContext +} + +func NewRateLogic(svcCtx *svc.ServiceContext) *RateLogic { + return &RateLogic{ + svcCtx: svcCtx, + } +} + +func (l *RateLogic) ProcessTask(ctx context.Context, _ *asynq.Task) error { + // Retrieve system currency configuration + currency, err := l.svcCtx.SystemModel.GetCurrencyConfig(ctx) + if err != nil { + logger.Errorw("[PurchaseCheckout] GetCurrencyConfig error", logger.Field("error", err.Error())) + return err + } + // Parse currency configuration + configs := struct { + CurrencyUnit string + CurrencySymbol string + AccessKey string + }{} + tool.SystemConfigSliceReflectToStruct(currency, &configs) + + // Skip conversion if no exchange rate API key configured + if configs.AccessKey == "" { + logger.Debugf("[RateLogic] skip exchange rate, no access key configured") + return nil + } + // Update exchange rates + result, err := exchangeRate.GetExchangeRete(configs.CurrencyUnit, "CNY", configs.AccessKey, 1) + if err != nil { + logger.Errorw("[RateLogic] GetExchangeRete error", logger.Field("error", err.Error())) + return err + } + l.svcCtx.ExchangeRate = result + logger.WithContext(ctx).Infof("[RateLogic] GetExchangeRete success, result: %+v", result) + return nil +} diff --git a/queue/logic/traffic/resetTrafficLogic.go b/queue/logic/traffic/resetTrafficLogic.go index 2332d65..bbfee15 100644 --- a/queue/logic/traffic/resetTrafficLogic.go +++ b/queue/logic/traffic/resetTrafficLogic.go @@ -2,17 +2,20 @@ package traffic import ( "context" + "encoding/json" "errors" "strconv" "strings" "time" - "github.com/hibiken/asynq" + "github.com/perfect-panel/server/internal/model/log" "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/hibiken/asynq" "github.com/redis/go-redis/v9" "gorm.io/gorm" ) @@ -116,7 +119,7 @@ func (l *ResetTrafficLogic) ProcessTask(ctx context.Context, _ *asynq.Task) erro // Load last reset time from cache var cache resetTrafficCache - err = l.svc.Redis.Get(ctx, cacheKey).Scan(&cache) + cacheData, err := l.svc.Redis.Get(ctx, cacheKey).Result() if err != nil { if !errors.Is(err, redis.Nil) { logger.Errorw("[ResetTraffic] Failed to get cache", logger.Field("error", err.Error())) @@ -127,7 +130,15 @@ func (l *ResetTrafficLogic) ProcessTask(ctx context.Context, _ *asynq.Task) erro } logger.Infow("[ResetTraffic] Using default cache value", logger.Field("lastResetTime", cache.LastResetTime)) } else { - logger.Infow("[ResetTraffic] Cache loaded successfully", logger.Field("lastResetTime", cache.LastResetTime)) + // Parse JSON data + if err := json.Unmarshal([]byte(cacheData), &cache); err != nil { + logger.Errorw("[ResetTraffic] Failed to unmarshal cache", logger.Field("error", err.Error())) + cache = resetTrafficCache{ + LastResetTime: time.Now().Add(-10 * time.Minute), + } + } else { + logger.Infow("[ResetTraffic] Cache loaded successfully", logger.Field("lastResetTime", cache.LastResetTime)) + } } // Execute reset operations in order: yearly -> monthly (1st) -> monthly (cycle) @@ -153,12 +164,17 @@ func (l *ResetTrafficLogic) ProcessTask(ctx context.Context, _ *asynq.Task) erro 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 + cacheDataBytes, marshalErr := json.Marshal(updatedCache) + if marshalErr != nil { + logger.Errorw("[ResetTraffic] Failed to marshal cache", logger.Field("error", marshalErr.Error())) } else { - logger.Infow("[ResetTraffic] Cache updated successfully", logger.Field("newLastResetTime", startTime)) + cacheErr := l.svc.Redis.Set(ctx, cacheKey, cacheDataBytes, 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 @@ -187,22 +203,19 @@ func (l *ResetTrafficLogic) resetMonth(ctx context.Context) error { var monthlyResetUsers []int64 // Check if today is the last day of current month - nextMonth := now.AddDate(0, 1, 0) - isLastDayOfMonth := nextMonth.Month() != now.Month() + isLastDayOfMonth := now.AddDate(0, 0, 1).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 + Where("`status` IN ?", []int64{1, 2}). // Only active subscriptions + Where("TIMESTAMPDIFF(MONTH, CURDATE(),DATE(expire_time)) >= 1") // At least 1 month passed if isLastDayOfMonth { // Last day of month: handle subscription start dates >= today - query = query.Where("DAY(start_time) >= ?", now.Day()) + query = query.Where("DAY(`expire_time`) >= ?", now.Day()) } else { // Normal case: exact day match - query = query.Where("DAY(start_time) = ?", now.Day()) + query = query.Where("DAY(`expire_time`) = ?", now.Day()) } err = query.Find(&monthlyResetUsers).Error @@ -218,20 +231,29 @@ func (l *ResetTrafficLogic) resetMonth(ctx context.Context) error { err = db.Model(&user.Subscribe{}).Where("`id` IN ?", monthlyResetUsers). Updates(map[string]interface{}{ - "upload": 0, - "download": 0, + "upload": 0, + "download": 0, + "status": 1, // Ensure status is active + "finished_at": nil, }).Error if err != nil { logger.Errorw("[ResetTraffic] Failed to update monthly reset users", logger.Field("error", err.Error())) return err } - + // Find user subscriptions for these users + var userSubs []*user.Subscribe + err = db.Model(&user.Subscribe{}).Where("`id` IN ?", monthlyResetUsers).Find(&userSubs).Error + if err != nil { + logger.Errorw("[ResetTraffic] Failed to find user subscriptions for 1st reset", logger.Field("error", err.Error())) + return err + } + // Clear cache for these subscriptions + l.clearCache(ctx, userSubs) logger.Infow("[ResetTraffic] Monthly reset completed", logger.Field("count", len(monthlyResetUsers))) } else { logger.Infow("[ResetTraffic] No users found for monthly reset") } - - return nil + return l.svc.SubscribeModel.ClearCache(ctx, resetMonthSubIds...) }) if err != nil { logger.Errorw("[ResetTraffic] Monthly reset transaction failed", logger.Field("error", err.Error())) @@ -279,7 +301,7 @@ func (l *ResetTrafficLogic) reset1st(ctx context.Context, cache resetTrafficCach var users1stReset []int64 err = db.Model(&user.Subscribe{}).Select("`id`"). Where("`subscribe_id` IN ?", reset1stSubIds). - Where("`status` = ?", 1). // Only active subscriptions + Where("`status` IN ?", []int64{1, 2}). // Only active subscriptions Find(&users1stReset).Error if err != nil { logger.Errorw("[ResetTraffic] Failed to query 1st reset users", logger.Field("error", err.Error())) @@ -294,27 +316,36 @@ func (l *ResetTrafficLogic) reset1st(ctx context.Context, cache resetTrafficCach // Reset upload and download traffic to zero err = db.Model(&user.Subscribe{}).Where("`id` IN ?", users1stReset). Updates(map[string]interface{}{ - "upload": 0, - "download": 0, + "upload": 0, + "download": 0, + "status": 1, // Ensure status is active + "finished_at": nil, }).Error if err != nil { logger.Errorw("[ResetTraffic] Failed to update 1st reset users", logger.Field("error", err.Error())) return err } + var userSubs []*user.Subscribe + err = db.Model(&user.Subscribe{}).Where("`id` IN ?", users1stReset).Find(&userSubs).Error + if err != nil { + logger.Errorw("[ResetTraffic] Failed to find user subscriptions for 1st reset", logger.Field("error", err.Error())) + return err + } + // Clear cache for these subscriptions + l.clearCache(ctx, userSubs) logger.Infow("[ResetTraffic] 1st reset completed", logger.Field("count", len(users1stReset))) } else { logger.Infow("[ResetTraffic] No users found for 1st reset") } - return nil + return l.svc.SubscribeModel.ClearCache(ctx, reset1stSubIds...) }) 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 } @@ -341,29 +372,20 @@ func (l *ResetTrafficLogic) resetYear(ctx context.Context) error { // 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 - + Where("MONTH(expire_time) = ?", now.Month()). // Same month + Where("`status` IN ?", []int64{1, 2}). // Only active subscriptions + Where("TIMESTAMPDIFF(YEAR, CURDATE(),DATE(expire_time)) >= 1") // At least 1 year passed 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()) + query = query.Where("DAY(expire_time) IN (28, 29)") } else { // Normal case: exact day match - query = query.Where("DAY(start_time) = ?", now.Day()) + query = query.Where("DAY(expire_time) = ?", now.Day()) } err = query.Find(&usersYearReset).Error @@ -380,19 +402,32 @@ func (l *ResetTrafficLogic) resetYear(ctx context.Context) error { // Reset upload and download traffic to zero err = db.Model(&user.Subscribe{}).Where("`id` IN ?", usersYearReset). Updates(map[string]interface{}{ - "upload": 0, - "download": 0, + "upload": 0, + "download": 0, + "status": 1, // Ensure status is active + "finished_at": nil, }).Error if err != nil { logger.Errorw("[ResetTraffic] Failed to update yearly reset users", logger.Field("error", err.Error())) return err } - + // Find user subscriptions for these users + var userSubs []*user.Subscribe + err = db.Model(&user.Subscribe{}).Where("`id` IN ?", usersYearReset).Find(&userSubs).Error + if err != nil { + logger.Errorw("[ResetTraffic] Failed to find user subscriptions for 1st reset", logger.Field("error", err.Error())) + return err + } + // Clear cache for these subscriptions + l.clearCache(ctx, userSubs) logger.Infow("[ResetTraffic] Yearly reset completed", logger.Field("count", len(usersYearReset))) } else { logger.Infow("[ResetTraffic] No users found for yearly reset") } - + err = l.svc.SubscribeModel.ClearCache(ctx, resetYearSubIds...) + if err != nil { + logger.Errorw("[ResetTraffic] Failed to clear yearly reset subscription cache", logger.Field("error", err.Error())) + } return nil }) @@ -538,3 +573,53 @@ func (l *ResetTrafficLogic) isRetryableError(err error) bool { logger.Field("error", err.Error())) return true } + +// clearCache clears the reset traffic cache +func (l *ResetTrafficLogic) clearCache(ctx context.Context, list []*user.Subscribe) { + if len(list) != 0 { + subs := make(map[int64]bool) + + for _, sub := range list { + if sub.SubscribeId > 0 { + err := l.svc.UserModel.ClearSubscribeCache(ctx, sub) + if err != nil { + logger.Errorw("[ResetTraffic] Failed to clear cache for subscription", + logger.Field("subscribeId", sub.SubscribeId), + logger.Field("error", err.Error())) + } + if _, ok := subs[sub.SubscribeId]; !ok { + subs[sub.SubscribeId] = true + } + } + // Insert traffic reset log + l.insertLog(ctx, sub.Id, sub.UserId) + } + + for sub, _ := range subs { + if err := l.svc.SubscribeModel.ClearCache(ctx, sub); err != nil { + logger.Errorw("[ResetTraffic] Failed to clear subscription cache", + logger.Field("subscribeId", sub), + logger.Field("error", err.Error()), + ) + } + } + } +} + +// insertLog inserts a reset traffic log entry +func (l *ResetTrafficLogic) insertLog(ctx context.Context, subId, userId int64) { + trafficLog := log.ResetSubscribe{ + Type: log.ResetSubscribeTypeAuto, + UserId: userId, + Timestamp: time.Now().UnixMilli(), + } + content, _ := trafficLog.Marshal() + if err := l.svc.DB.WithContext(ctx).Model(&log.SystemLog{}).Create(&log.SystemLog{ + Type: log.TypeResetSubscribe.Uint8(), + ObjectID: subId, + Date: time.Now().Format(time.DateOnly), + Content: string(content), + }).Error; err != nil { + logger.Errorw("[ResetTraffic] Failed to create system log for subscription", logger.Field("error", err.Error())) + } +} diff --git a/queue/logic/traffic/serverDataLogic.go b/queue/logic/traffic/serverDataLogic.go index 98fbaba..f8ca675 100644 --- a/queue/logic/traffic/serverDataLogic.go +++ b/queue/logic/traffic/serverDataLogic.go @@ -73,7 +73,7 @@ func (l *ServerDataLogic) getRanking(ctx context.Context) (top10ServerToday, top if s.ServerId == 0 { continue } - serverInfo, err := l.svc.ServerModel.FindOne(ctx, s.ServerId) + serverInfo, err := l.svc.NodeModel.FindOneServer(ctx, s.ServerId) if err != nil { logger.Error("[ServerDataLogic] Find server failed", logger.Field("error", err.Error())) continue @@ -92,7 +92,7 @@ func (l *ServerDataLogic) getRanking(ctx context.Context) (top10ServerToday, top logger.Error("[ServerDataLogic] Get top servers traffic by day failed", logger.Field("error", err.Error())) } else { for _, s := range serverYesterday { - serverInfo, err := l.svc.ServerModel.FindOne(ctx, s.ServerId) + serverInfo, err := l.svc.NodeModel.FindOneServer(ctx, s.ServerId) if err != nil { logger.Error("[ServerDataLogic] Find server failed", logger.Field("error", err.Error())) continue diff --git a/queue/logic/traffic/trafficStatLogic.go b/queue/logic/traffic/trafficStatLogic.go new file mode 100644 index 0000000..e5357a8 --- /dev/null +++ b/queue/logic/traffic/trafficStatLogic.go @@ -0,0 +1,176 @@ +package traffic + +import ( + "context" + "time" + + "github.com/hibiken/asynq" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/model/traffic" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/logger" +) + +type StatLogic struct { + svc *svc.ServiceContext +} + +func NewStatLogic(svc *svc.ServiceContext) *StatLogic { + return &StatLogic{ + svc: svc, + } +} + +func (l *StatLogic) ProcessTask(ctx context.Context, _ *asynq.Task) error { + now := time.Now() + tx := l.svc.DB.Begin() + var err error + defer func(err error) { + if err != nil { + logger.Errorf("[Traffic Stat Queue] Process task failed: %v", err.Error()) + tx.Rollback() + } else { + logger.Infof("[Traffic Stat Queue] Process task completed successfully, consuming: %s", time.Since(now).String()) + // 提交事务 + if err = tx.Commit().Error; err != nil { + logger.Errorf("[Traffic Stat Queue] Commit transaction failed: %v", err.Error()) + } + } + }(err) + + // 获取全部有效订阅 + var userTraffic []log.UserTraffic + // 获取统计时间范围 + start := time.Date(now.Year(), now.Month(), now.Day()-1, 0, 0, 0, 0, time.Local) + end := start.Add(24 * time.Hour).Add(-time.Nanosecond) + + // 查询用户流量统计, 按用户和订阅分组 + err = tx.WithContext(ctx).Model(&traffic.TrafficLog{}). + Select("user_id, subscribe_id, SUM(download + upload) AS total, SUM(download) AS download, SUM(upload) AS upload"). + Where("timestamp BETWEEN ? AND ?", start, end). + Group("user_id, subscribe_id"). + Order("total DESC"). + Scan(&userTraffic).Error + if err != nil { + logger.Errorf("[Traffic Stat Queue] Query user traffic failed: %v", err.Error()) + return err + } + + date := start.Format(time.DateOnly) + + userTop10 := log.UserTrafficRank{ + Rank: make(map[uint8]log.UserTraffic), + } + + // 更新用户流量统计 + for i, trafficData := range userTraffic { + if i < 10 { + userTop10.Rank[uint8(i+1)] = trafficData + } + // 更新用户流量统计日志 + content, _ := trafficData.Marshal() + err = tx.WithContext(ctx).Model(&log.SystemLog{}).Create(&log.SystemLog{ + Type: log.TypeSubscribeTraffic.Uint8(), + Date: date, + ObjectID: trafficData.SubscribeId, + Content: string(content), + }).Error + if err != nil { + logger.Errorf("[Traffic Stat Queue] Create user traffic log failed: %v", err.Error()) + return err + } + } + + userTop10Content, _ := userTop10.Marshal() + + // 更新用户排行榜 + err = tx.WithContext(ctx).Model(&log.SystemLog{}).Create(&log.SystemLog{ + Type: log.TypeUserTrafficRank.Uint8(), + Date: date, + ObjectID: 0, // 0表示全局用户排行榜 + Content: string(userTop10Content), + }).Error + if err != nil { + logger.Errorf("[Traffic Stat Queue] Create user traffic rank log failed: %v", err.Error()) + return err + } + + // 统计服务器流量 + var serverTraffic []log.ServerTraffic + err = tx.WithContext(ctx).Model(&traffic.TrafficLog{}). + Select("server_id, SUM(download + upload) AS total, SUM(download) AS download, SUM(upload) AS upload"). + Where("timestamp BETWEEN ? AND ?", start, end). + Group("server_id"). + Order("total DESC"). + Scan(&serverTraffic).Error + if err != nil { + logger.Errorf("[Traffic Stat Queue] Query server traffic failed: %v", err.Error()) + return err + } + + serverTop10 := log.ServerTrafficRank{ + Rank: make(map[uint8]log.ServerTraffic), + } + for i, trafficData := range serverTraffic { + if i < 10 { + serverTop10.Rank[uint8(i+1)] = trafficData + } + // 更新服务器流量统计日志 + content, _ := trafficData.Marshal() + err = tx.WithContext(ctx).Model(&log.SystemLog{}).Create(&log.SystemLog{ + Type: log.TypeServerTraffic.Uint8(), + Date: date, + ObjectID: trafficData.ServerId, + Content: string(content), + }).Error + if err != nil { + logger.Errorf("[Traffic Stat Queue] Create server traffic log failed: %v", err.Error()) + return err + } + } + serverTop10Content, _ := serverTop10.Marshal() + // 更新服务器排行榜 + err = tx.WithContext(ctx).Model(&log.SystemLog{}).Create(&log.SystemLog{ + Type: log.TypeServerTrafficRank.Uint8(), + Date: date, + ObjectID: 0, // 0表示全局服务器排行榜 + Content: string(serverTop10Content), + }).Error + if err != nil { + logger.Errorf("[Traffic Stat Queue] Create server traffic rank log failed: %v", err.Error()) + return err + } + + // traffic stat + var stat log.TrafficStat + err = tx.WithContext(ctx).Model(&traffic.TrafficLog{}). + Select("SUM(download + upload) AS total, SUM(download) AS download, SUM(upload) AS upload"). + Where("timestamp BETWEEN ? AND ?", start, end). + Scan(&stat).Error + if err != nil { + logger.Errorf("[Traffic Stat Queue] Query traffic stat failed: %v", err.Error()) + return err + } + + // 更新流量统计日志 + content, _ := stat.Marshal() + err = tx.WithContext(ctx).Model(&log.SystemLog{}).Create(&log.SystemLog{ + Type: log.TypeTrafficStat.Uint8(), + Date: date, + ObjectID: 0, + Content: string(content), + }).Error + if err != nil { + logger.Errorf("[Traffic Stat Queue] Create traffic stat log failed: %v", err.Error()) + return err + } + + // Delete old traffic logs + if l.svc.Config.Log.AutoClear { + err = tx.WithContext(ctx).Model(&traffic.TrafficLog{}).Where("timestamp <= ?", end.AddDate(0, 0, int(-l.svc.Config.Log.ClearDays))).Delete(&traffic.TrafficLog{}).Error + if err != nil { + logger.Errorf("[Traffic Stat Queue] Delete server traffic log failed: %v", err.Error()) + } + } + return nil +} diff --git a/queue/logic/traffic/trafficStatisticsLogic.go b/queue/logic/traffic/trafficStatisticsLogic.go index 07509f0..ed98cd1 100644 --- a/queue/logic/traffic/trafficStatisticsLogic.go +++ b/queue/logic/traffic/trafficStatisticsLogic.go @@ -3,8 +3,10 @@ package traffic import ( "context" "encoding/json" + "strings" "time" + "github.com/perfect-panel/server/internal/model/node" "github.com/perfect-panel/server/pkg/logger" "github.com/hibiken/asynq" @@ -38,7 +40,7 @@ func (l *TrafficStatisticsLogic) ProcessTask(ctx context.Context, task *asynq.Ta return nil } // query server info - serverInfo, err := l.svc.ServerModel.FindOne(ctx, payload.ServerId) + serverInfo, err := l.svc.NodeModel.FindOneServer(ctx, payload.ServerId) if err != nil { logger.WithContext(ctx).Error("[TrafficStatistics] Find server info failed", logger.Field("serverId", payload.ServerId), @@ -46,27 +48,38 @@ func (l *TrafficStatisticsLogic) ProcessTask(ctx context.Context, task *asynq.Ta ) return nil } - if serverInfo.TrafficRatio == 0 { - logger.WithContext(ctx).Error("[TrafficStatistics] Server log ratio is 0", - logger.Field("serverId", payload.ServerId), - ) + // query protocol ratio + // default ratio is 1.0 + + protocols, err := serverInfo.UnmarshalProtocols() + if err != nil { + logger.Errorf("[TrafficStatistics] Unmarshal protocols failed: %s", err.Error()) return nil } + var protocol *node.Protocol + + var ratio float32 = 1.0 + + for _, p := range protocols { + if strings.ToLower(p.Type) == strings.ToLower(payload.Protocol) { + protocol = &p + break + } + } + + if protocol == nil { + logger.WithContext(ctx).Error("[TrafficStatistics] Protocol not found: %s", payload.Protocol) + return nil + } + + // use protocol ratio if it's greater than 0 + if protocol.Ratio > 0 { + ratio = float32(protocol.Ratio) + } + now := time.Now() realTimeMultiplier := l.svc.NodeMultiplierManager.GetMultiplier(now) for _, log := range payload.Logs { - // update user subscribe with log - d := int64(float32(log.Download) * serverInfo.TrafficRatio * realTimeMultiplier) - u := int64(float32(log.Upload) * serverInfo.TrafficRatio * realTimeMultiplier) - if err := l.svc.UserModel.UpdateUserSubscribeWithTraffic(ctx, log.SID, d, u); err != nil { - logger.WithContext(ctx).Error("[TrafficStatistics] Update user subscribe with log failed", - logger.Field("sid", log.SID), - logger.Field("download", float32(log.Download)*serverInfo.TrafficRatio), - logger.Field("upload", float32(log.Upload)*serverInfo.TrafficRatio), - logger.Field("error", err.Error()), - ) - continue - } // query user Subscribe Info sub, err := l.svc.UserModel.FindOneSubscribe(ctx, log.SID) if err != nil { @@ -77,8 +90,25 @@ func (l *TrafficStatisticsLogic) ProcessTask(ctx context.Context, task *asynq.Ta continue } + if log.Download+log.Upload <= l.svc.Config.Node.TrafficReportThreshold { + // no traffic, skip + continue + } + // update user subscribe with log + d := int64(float32(log.Download) * ratio * realTimeMultiplier) + u := int64(float32(log.Upload) * ratio * realTimeMultiplier) + if err := l.svc.UserModel.UpdateUserSubscribeWithTraffic(ctx, sub.Id, d, u); err != nil { + logger.WithContext(ctx).Error("[TrafficStatistics] Update user subscribe with log failed", + logger.Field("sid", log.SID), + logger.Field("download", float32(log.Download)*ratio), + logger.Field("upload", float32(log.Upload)*ratio), + logger.Field("error", err.Error()), + ) + continue + } + // create log log - if err := l.svc.TrafficLogModel.Insert(ctx, &traffic.TrafficLog{ + if err = l.svc.TrafficLogModel.Insert(ctx, &traffic.TrafficLog{ ServerId: payload.ServerId, SubscribeId: log.SID, UserId: sub.UserId, @@ -88,8 +118,8 @@ func (l *TrafficStatisticsLogic) ProcessTask(ctx context.Context, task *asynq.Ta }); err != nil { logger.WithContext(ctx).Error("[TrafficStatistics] Create log log failed", logger.Field("uid", log.SID), - logger.Field("download", float32(log.Download)*serverInfo.TrafficRatio), - logger.Field("upload", float32(log.Upload)*serverInfo.TrafficRatio), + logger.Field("download", float32(log.Download)*ratio), + logger.Field("upload", float32(log.Upload)*ratio), logger.Field("error", err.Error()), ) } diff --git a/queue/types/country.go b/queue/types/country.go deleted file mode 100644 index 13b9e0c..0000000 --- a/queue/types/country.go +++ /dev/null @@ -1,11 +0,0 @@ -package types - -const ( - // ForthwithGetCountry forthwith country get - ForthwithGetCountry = "forthwith:country:get" -) - -type GetNodeCountry struct { - Protocol string `json:"protocol"` - ServerAddr string `json:"server_addr"` -} diff --git a/queue/types/email.go b/queue/types/email.go index 2cbdf2e..4fee979 100644 --- a/queue/types/email.go +++ b/queue/types/email.go @@ -5,10 +5,19 @@ const ( ForthwithSendEmail = "forthwith:email:send" ) +const ( + EmailTypeVerify = "verify" + EmailTypeMaintenance = "maintenance" + EmailTypeExpiration = "expiration" + EmailTypeTrafficExceed = "traffic_exceed" + EmailTypeCustom = "custom" +) + type ( SendEmailPayload struct { - Email string `json:"to"` - Subject string `json:"subject"` - Content string `json:"content"` + Type string `json:"type"` + Email string `json:"to"` + Subject string `json:"subject"` + Content map[string]interface{} `json:"content"` } ) diff --git a/queue/types/scheduler.go b/queue/types/scheduler.go index 26a32f0..51ef48c 100644 --- a/queue/types/scheduler.go +++ b/queue/types/scheduler.go @@ -4,4 +4,5 @@ const ( SchedulerCheckSubscription = "scheduler:check:subscription" SchedulerTotalServerData = "scheduler:total:server" SchedulerResetTraffic = "scheduler:reset:traffic" + SchedulerTrafficStat = "scheduler:traffic:stat" ) diff --git a/queue/types/server.go b/queue/types/server.go index 75ec89c..3202ed1 100644 --- a/queue/types/server.go +++ b/queue/types/server.go @@ -10,6 +10,7 @@ type UserTraffic struct { type TrafficStatistics struct { ServerId int64 `json:"server_id"` + Protocol string `json:"protocol"` Logs []UserTraffic `json:"logs"` } diff --git a/queue/types/sms.go b/queue/types/sms.go index 8d9f9e7..affb521 100644 --- a/queue/types/sms.go +++ b/queue/types/sms.go @@ -1,7 +1,7 @@ package types const ( - // ForthwithSendEmail forthwith send email + // ForthwithSendSms forthwith send email ForthwithSendSms = "forthwith:sms:send" ) diff --git a/queue/types/task.go b/queue/types/task.go new file mode 100644 index 0000000..85da331 --- /dev/null +++ b/queue/types/task.go @@ -0,0 +1,12 @@ +package types + +const ( + // ScheduledBatchSendEmail scheduled batch send email + ScheduledBatchSendEmail = "scheduled:email:batch" + + // ForthwithQuotaTask create quota task immediately + ForthwithQuotaTask = "forthwith:quota:task" + + // SchedulerExchangeRate fetch exchange rate task + SchedulerExchangeRate = "scheduler:exchange:rate" +) diff --git a/readme_zh.md b/readme_zh.md index 8add99d..ee4a1c4 100644 --- a/readme_zh.md +++ b/readme_zh.md @@ -14,6 +14,19 @@ +> **第一条** +> 人人生而自由,在尊严与权利上一律平等。 +> 他们赋有理性与良知,应当以兄弟般的精神彼此相待。 +> +> **第十二条** +> 任何人的隐私、家庭、住宅和通信不得任意干涉,其名誉与荣誉不得加以攻击。 +> 人人有权受到法律的保护,以免遭受这种干涉或攻击。 +> +> **第十九条** +> 人人有思想与表达的自由;此项自由包括持有主张而不受干预,以及通过任何媒介、无论国界,自由寻求、接受和传播信息与思想。 +> +> *来源: [United Nations – Universal Declaration of Human Rights (UN.org)](https://www.un.org/sites/un2.un.org/files/2021/03/udhr.pdf)* + ## 📋 概述 PPanel 服务端是 PPanel 项目的后端组件,为代理服务提供强大的 API 和核心功能。它基于 Go 语言开发,注重性能、安全性和可扩展性。 @@ -100,8 +113,8 @@ PPanel 服务端是 PPanel 项目的后端组件,为代理服务提供强大 4. **从 Docker Hub 拉取**(CI/CD 发布后): ```bash - docker pull yourusername/ppanel-server:latest - docker run --rm -p 8080:8080 yourusername/ppanel-server:latest + docker pull ppanel/ppanel-server:latest + docker run --rm -p 8080:8080 ppanel/ppanel-server:latest ``` ## 📖 API 文档 diff --git a/scheduler/scheduler.go b/scheduler/scheduler.go index 69a4572..bf131e8 100644 --- a/scheduler/scheduler.go +++ b/scheduler/scheduler.go @@ -29,17 +29,29 @@ func (m *Service) Start() { if _, err := m.server.Register("@every 60s", checkTask); err != nil { logger.Errorf("register check subscription task failed: %s", err.Error()) } - // schedule total server data task: every 5 minutes - totalServerDataTask := asynq.NewTask(types.SchedulerTotalServerData, nil) - if _, err := m.server.Register("@every 180s", totalServerDataTask); err != nil { - logger.Errorf("register total server data task failed: %s", err.Error()) - } - // schedule reset traffic task: every 24 hours + //// schedule total server data task: every 5 minutes + //totalServerDataTask := asynq.NewTask(types.SchedulerTotalServerData, nil) + //if _, err := m.server.Register("@every 180s", totalServerDataTask); err != nil { + // logger.Errorf("register total server data task failed: %s", err.Error()) + //} + // schedule reset traffic task: every day at 00:30 resetTrafficTask := asynq.NewTask(types.SchedulerResetTraffic, nil) - if _, err := m.server.Register("@every 24h", resetTrafficTask); err != nil { + if _, err := m.server.Register("30 0 * * *", resetTrafficTask); err != nil { logger.Errorf("register reset traffic task failed: %s", err.Error()) } + // schedule traffic stat task: every day at 00:00 + trafficStatTask := asynq.NewTask(types.SchedulerTrafficStat, nil) + if _, err := m.server.Register("0 0 * * *", trafficStatTask, asynq.MaxRetry(3)); err != nil { + logger.Errorf("register traffic stat task failed: %s", err.Error()) + } + + // schedule update exchange rate task: every day at 01:00 + rateTask := asynq.NewTask(types.ForthwithQuotaTask, nil) + if _, err := m.server.Register("0 1 * * *", rateTask, asynq.MaxRetry(3)); err != nil { + logger.Errorf("register update exchange rate task failed: %s", err.Error()) + } + if err := m.server.Run(); err != nil { logger.Errorf("run scheduler failed: %s", err.Error()) } diff --git a/script/generate.sh b/script/generate.sh index ea116d5..26fd79a 100755 --- a/script/generate.sh +++ b/script/generate.sh @@ -6,9 +6,13 @@ ARCH_TYPE=$(uname -m) if [[ "$OS_TYPE" == "Linux" ]]; then echo "The current operating system is Linux" if [[ "$ARCH_TYPE" == "x86_64" ]]; then + echo "Format api file" + ./generate/gopure-linux-amd64 api format --dir ./apis echo "Architecture: amd64" ./generate/gopure-linux-amd64 api go -api *.api -dir . -style goZero elif [[ "$ARCH_TYPE" == "aarch64" ]]; then + echo "Format api file" + ./generate/gopure-linux-arm64 api format --dir ./apis echo "Architecture: arm64" ./generate/gopure-linux-arm64 api go -api *.api -dir . -style goZero else @@ -17,9 +21,13 @@ if [[ "$OS_TYPE" == "Linux" ]]; then elif [[ "$OS_TYPE" == "Darwin" ]]; then echo "The current operating system is macOS" if [[ "$ARCH_TYPE" == "x86_64" ]]; then + echo "Format api file" + ./generate/gopure-darwin-amd64 api format --dir ./apis echo "Architecture: amd64" ./generate/gopure-darwin-amd64 api go -api *.api -dir . -style goZero elif [[ "$ARCH_TYPE" == "arm64" ]]; then + echo "Format api file" + ./generate/gopure-darwin-arm64 api format --dir ./apis echo "Architecture: arm64" ./generate/gopure-darwin-arm64 api go -api *.api -dir . -style goZero else @@ -28,9 +36,13 @@ elif [[ "$OS_TYPE" == "Darwin" ]]; then elif [[ "$OS_TYPE" == "CYGWIN"* || "$OS_TYPE" == "MINGW"* ]]; then echo "The current operating system is Windows" if [[ "$ARCH_TYPE" == "x86_64" ]]; then + echo "Format api file" + ./generate/gopure-amd64.exe api format --dir ./apis echo "Architecture: amd64" ./generate/gopure-amd64.exe api go -api *.api -dir . -style goZero elif [[ "$ARCH_TYPE" == "arm64" ]]; then + echo "Format api file" + ./generate/gopure-arm64.exe api format --dir ./apis echo "Architecture: arm64" ./generate/gopure-arm64.exe api go -api *.api -dir . -style goZero else