diff --git a/apis/admin/tool.api b/apis/admin/tool.api index da1916b..4d1f17b 100644 --- a/apis/admin/tool.api +++ b/apis/admin/tool.api @@ -17,6 +17,14 @@ type ( 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 ( @@ -36,5 +44,9 @@ service ppanel { @doc "Get Version" @handler GetVersion get /version returns (VersionResponse) + + @doc "Query IP Location" + @handler QueryIPLocation + get /ip/location (QueryIPLocationRequest) returns (QueryIPLocationResponse) } diff --git a/etc/ppanel.yaml b/etc/ppanel.yaml index e69de29..daba947 100644 --- a/etc/ppanel.yaml +++ b/etc/ppanel.yaml @@ -0,0 +1,39 @@ +Host: 0.0.0.0 +Port: 8080 +TLS: + Enable: false + CertFile: "" + KeyFile: "" +Debug: false +JwtAuth: + AccessSecret: 4672dccd-2c50-4833-8e17-81e6c2119893 + AccessExpire: 604800 +Logger: + ServiceName: PPanel + Mode: console + Encoding: plain + TimeFormat: "2006-01-02 15:04:05.000" + Path: logs + Level: info + MaxContentLength: 0 + Compress: false + Stat: true + KeepDays: 0 + StackCooldownMillis: 100 + MaxBackups: 0 + MaxSize: 0 + Rotation: daily + FileTimeFormat: 2006-01-02T15:04:05.000Z07:00 +MySQL: + Addr: localhost:3306 + Username: root + Password: password + Dbname: ppanel_dev + Config: charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai + MaxIdleConns: 10 + MaxOpenConns: 10 + SlowThreshold: 1000 +Redis: + Host: 127.0.0.1:6379 + Pass: "" + DB: 0 diff --git a/go.mod b/go.mod index 3e110be..9d0b191 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 6ab9d74..aa84f0e 100644 --- a/go.sum +++ b/go.sum @@ -285,6 +285,10 @@ github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQ github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/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= 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/common/getClientHandler.go b/internal/handler/common/getClientHandler.go index e40b555..78d613d 100644 --- a/internal/handler/common/getClientHandler.go +++ b/internal/handler/common/getClientHandler.go @@ -10,7 +10,6 @@ import ( // Get Client func GetClientHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - l := common.NewGetClientLogic(c.Request.Context(), svcCtx) resp, err := l.GetClient() result.HttpResult(c, resp, err) 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/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 be05079..4f6bc3a 100644 --- a/internal/svc/serviceContext.go +++ b/internal/svc/serviceContext.go @@ -37,6 +37,7 @@ type ServiceContext struct { Config config.Config Queue *asynq.Client ExchangeRate float64 + GeoIP *IPLocation //NodeCache *cache.NodeCacheClient AuthModel auth.Model @@ -68,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, @@ -89,6 +98,7 @@ func NewServiceContext(c config.Config) *ServiceContext { Config: c, Queue: NewAsynqClient(c), ExchangeRate: 1.0, + GeoIP: geoIP, //NodeCache: cache.NewNodeCacheClient(rds), AuthLimiter: authLimiter, AdsModel: ads.NewModel(db, rds), diff --git a/internal/types/types.go b/internal/types/types.go index dfe28bd..1c54b3f 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -1571,6 +1571,16 @@ 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:"regio,omitempty"` + City string `json:"city"` +} + type QueryNodeTagResponse struct { Tags []string `json:"tags"` } @@ -2598,6 +2608,7 @@ type UserSubscribe struct { Token string `json:"token"` Status uint8 `json:"status"` Short string `json:"short"` + Note string `json:"note"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` } @@ -2617,6 +2628,7 @@ type UserSubscribeDetail struct { Upload int64 `json:"upload"` Token string `json:"token"` Status uint8 `json:"status"` + Note string `json:"note"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` } @@ -2635,6 +2647,7 @@ type UserSubscribeInfo struct { Upload int64 `json:"upload"` Token string `json:"token"` Status uint8 `json:"status"` + Note string `json:"note"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` IsTryOut bool `json:"is_try_out"`