diff --git a/api/network/model/ip_info.go b/api/network/model/ip_info.go new file mode 100644 index 0000000..43dbfad --- /dev/null +++ b/api/network/model/ip_info.go @@ -0,0 +1,11 @@ +package model + +// IpInfo // IP信息 +type IpInfo struct { + IP string `json:"ip"` // IP地址 + Country string `json:"country"` // 国家 + Region string `json:"region"` // 区域 + Province string `json:"province"` // 省份 + City string `json:"city"` // 城市 + ISP string `json:"ISP"` // ISP +} diff --git a/api/network/model/req/ip.go b/api/network/model/req/ip.go new file mode 100644 index 0000000..f83be48 --- /dev/null +++ b/api/network/model/req/ip.go @@ -0,0 +1,6 @@ +package req + +// GetIpInfoRequest 获取IP信息请求 +type GetIpInfoRequest struct { + IP string `json:"ip" validate:"required"` // IP 地址 +} diff --git a/api/network/server/ip_api.go b/api/network/server/ip_api.go new file mode 100644 index 0000000..ca28104 --- /dev/null +++ b/api/network/server/ip_api.go @@ -0,0 +1,47 @@ +package server + +import ( + "api/api/network/model" + "api/api/network/model/req" + "api/utils" + "api/utils/r" + "bufio" + "fmt" + "github.com/gin-gonic/gin" +) + +// IpApi ip接口 +type IpApi struct { +} + +// GetIpInfo 获取IP信息 +func (*IpApi) GetIpInfo(c *gin.Context) { + data := utils.BindValidJson[req.GetIpInfoRequest](c) + ipInfo, err := ipService.GetIpInfo(data.IP) + if err != nil { + r.Error(c) + } + r.SuccessData(c, ipInfo) + return +} + +// GetIpInfoList 获取IP信息 +func (*IpApi) GetIpInfoList(c *gin.Context) { + scanner := bufio.NewScanner(c.Request.Body) + var ipInfoList []model.IpInfo + + for scanner.Scan() { + line := scanner.Text() + ipInfo, err := ipService.GetIpInfo(line) + if err != nil { + continue + } + if ipInfo.ISP == "谷歌" { + ipInfoList = append(ipInfoList, ipInfo) + fmt.Println(ipInfo.IP) + } + // 在这里你可以处理每一行的数据,执行你的业务逻辑 + } + r.SuccessData(c, ipInfoList) + return +} diff --git a/api/network/server/z_enter.go b/api/network/server/z_enter.go index 494ca49..5aaf307 100644 --- a/api/network/server/z_enter.go +++ b/api/network/server/z_enter.go @@ -4,4 +4,5 @@ import "api/api/network/service" var ( networkService = service.NetWorkService{} + ipService = service.IpService{} ) diff --git a/api/network/service/ip_service.go b/api/network/service/ip_service.go new file mode 100644 index 0000000..2cf6b58 --- /dev/null +++ b/api/network/service/ip_service.go @@ -0,0 +1,54 @@ +package service + +import ( + "api/api/network/model" + "fmt" + "github.com/lionsoul2014/ip2region/binding/golang/xdb" + "log" + "strings" +) + +var ( + searcher *xdb.Searcher +) + +type IpService struct{} + +func init() { + var dbPath = "config/google.xdb" + // 1、从 dbPath 加载整个 xdb 到内存 + cBuff, err := xdb.LoadContentFromFile(dbPath) + if err != nil { + fmt.Printf("failed to load content from `%s`: %s\n", dbPath, err) + if err != nil { + log.Panic("dbPath初始化失败: ", err) + } + } + + // 2、用全局的 cBuff 创建完全基于内存的查询对象。 + searcher, err = xdb.NewWithBuffer(cBuff) + if err != nil { + fmt.Printf("failed to create searcher with content: %s\n", err) + if err != nil { + log.Panic("searcher初始化失败: ", err) + } + } +} + +// GetIpInfo 获取IP信息 +func (*IpService) GetIpInfo(ip string) (model.IpInfo, error) { + region, err := searcher.SearchByStr(ip) + if err != nil { + return model.IpInfo{}, err + } + info := strings.Split(region, "|") + var ipInfo = model.IpInfo{ + IP: ip, + Country: info[0], + Region: info[1], + Province: info[2], + City: info[3], + ISP: info[4], + } + return ipInfo, nil +} diff --git a/config/google.xdb b/config/google.xdb new file mode 100644 index 0000000..f4c97eb Binary files /dev/null and b/config/google.xdb differ diff --git a/go.mod b/go.mod index b752326..82ceb67 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20240510055607-89e20ab7b6c6 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect diff --git a/go.sum b/go.sum index af0c344..ea3ea12 100644 --- a/go.sum +++ b/go.sum @@ -49,6 +49,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20240510055607-89e20ab7b6c6 h1:YeIGErDiB/fhmNsJy0cfjoT8XnRNT9hb19xZ4MvWQDU= +github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20240510055607-89e20ab7b6c6/go.mod h1:C5LA5UO2ZXJrLaPLYtE1wUJMiyd/nwWaCO5cw/2pSHs= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= diff --git a/router/server_router.go b/router/server_router.go index cfa0cd8..0236a9b 100644 --- a/router/server_router.go +++ b/router/server_router.go @@ -18,6 +18,8 @@ func ServerRouter() http.Handler { base := r.Group("/api") { base.GET("/getHeaders", networkApi.GetHeaders) + base.POST("/getIpInfo", ipApi.GetIpInfo) + base.POST("/getIpInfoList", ipApi.GetIpInfoList) } return r } diff --git a/router/z_enter.go b/router/z_enter.go index c06b9c8..773ef48 100644 --- a/router/z_enter.go +++ b/router/z_enter.go @@ -6,4 +6,5 @@ import ( var ( networkApi server.NetWorkApi + ipApi server.IpApi ) diff --git a/utils/gin_context.go b/utils/gin_context.go new file mode 100644 index 0000000..b042aaf --- /dev/null +++ b/utils/gin_context.go @@ -0,0 +1,134 @@ +package utils + +import ( + "api/utils/model/req" + "api/utils/r" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" +) + +// 在内部 panic 会被 middleware 捕获到并返回错误信息 + +// Validate 参数合法性校验 +func Validate(c *gin.Context, data any) { + validMsg := Validator.Validate(data) + if validMsg != "" { + r.ReturnJson(c, http.StatusOK, r.ERROR_INVALID_PARAM, validMsg, nil) + panic(nil) + } +} + +// Bind 绑定 +func Bind[T any](c *gin.Context) (data T) { + if err := c.ShouldBind(&data); err != nil { + // Logger.Error("Bind", zap.Error(err)) + panic(r.ERROR_REQUEST_PARAM) + } + return +} + +// BindValid 绑定验证 + 合法性校验 +func BindValid[T any](c *gin.Context) (data T) { + // Json 绑定 + if err := c.ShouldBind(&data); err != nil { + // Logger.Error("BindValid", zap.Error(err)) + panic(r.ERROR_REQUEST_PARAM) + } + // 参数合法性校验 + Validate(c, &data) + return data +} + +// BindJson Json 绑定 +func BindJson[T any](c *gin.Context) (data T) { + if err := c.ShouldBindJSON(&data); err != nil { + // Logger.Error("BindJson", zap.Error(err)) + panic(r.ERROR_REQUEST_PARAM) + } + return +} + +// BindValidJson Json 绑定验证 + 合法性校验 +func BindValidJson[T any](c *gin.Context) (data T) { + // Json 绑定 + if err := c.ShouldBindJSON(&data); err != nil { + // Logger.Error("BindValidJson", zap.Error(err)) + panic(r.ERROR_REQUEST_PARAM) + } + // 参数合法性校验 + Validate(c, &data) + return data +} + +// BindQuery Param 绑定 +func BindQuery[T any](c *gin.Context) (data T) { + if err := c.ShouldBindQuery(&data); err != nil { + // Logger.Error("BindQuery", zap.Error(err)) + panic(r.ERROR_REQUEST_PARAM) + } + + // TODO: 检查是否有 PageSize 或 PageQuery 字段,并处理其值 + // val := reflect.ValueOf(data) + // pageSize := val.FieldByName("PageSize").Int() + // fmt.Println("pageSize: ", pageSize) + // val.FieldByName("PageSize").Elem().SetInt(12) + return +} + +// BindPageQuery Param 分页绑定(处理了 PageSize 和 PageQuery) +func BindPageQuery(c *gin.Context) (data req.PageQuery) { + if err := c.ShouldBindQuery(&data); err != nil { + // Logger.Error("BindQuery", zap.Error(err)) + panic(r.ERROR_REQUEST_PARAM) + } + // 检查分页参数 + CheckQueryPage(&data.PageSize, &data.PageNum) + return +} + +// BindValidQuery Param 绑定验证 + 合法性校验 +func BindValidQuery[T any](c *gin.Context) (data T) { + // Query 绑定 + if err := c.ShouldBindQuery(&data); err != nil { + // Logger.Error("BindValidQuery", zap.Error(err)) + panic(r.ERROR_REQUEST_PARAM) + } + // 参数合法性校验 + Validate(c, &data) + return data +} + +// CheckQueryPage 检查分页参数 +func CheckQueryPage(pageSize, pageNum *int) { + switch { + case *pageSize >= 100: + *pageSize = 100 + case *pageSize <= 0: + *pageSize = 10 + } + if *pageNum <= 0 { + *pageNum = 1 + } +} + +// GetFromContext 从 Gin Context 上获取值, 该值是 JWT middleware 解析 Token 后设置的 +// 如果该值不存在, 说明 Token 有问题 +func GetFromContext[T any](c *gin.Context, key string) T { + val, exist := c.Get(key) + if !exist { + panic(r.ERROR_TOKEN_RUNTIME) + } + return val.(T) +} + +// GetIntParam 从 Context 获取 Int 类型 Param 参数 +func GetIntParam(c *gin.Context, key string) int { + val, err := strconv.Atoi(c.Param(key)) + if err != nil { + // Logger.Error("GetIntParam", zap.Error(err)) + panic(r.ERROR_REQUEST_PARAM) + } + return val +} diff --git a/utils/model/req/universal.go b/utils/model/req/universal.go new file mode 100644 index 0000000..b54c9cf --- /dev/null +++ b/utils/model/req/universal.go @@ -0,0 +1,25 @@ +package req + +// KeywordQuery 关键字查询 +type KeywordQuery struct { + Keyword string `form:"keyword"` +} + +// PageQuery 获取数据(需要分页) +type PageQuery struct { + PageSize int `form:"page_size"` + PageNum int `form:"page_num"` + Keyword string `form:"keyword"` +} + +// SoftDelete 软删除请求(批量) +type SoftDelete struct { + Ids []int `json:"ids"` + IsDelete *int8 `json:"is_delete" validate:"required,min=0,max=1"` // 软删除到回收站, 没有的字段不使用 +} + +// UpdateReview 修改审核(批量) +type UpdateReview struct { + Ids []int `json:"ids"` + IsReview *int8 `json:"is_review" validate:"required,min=0,max=1"` +} diff --git a/utils/r/result.go b/utils/r/result.go index c7f90c3..dba9325 100644 --- a/utils/r/result.go +++ b/utils/r/result.go @@ -47,3 +47,13 @@ func SuccessData(c *gin.Context, data any) { func Success(c *gin.Context) { Send(c, http.StatusOK, OK, nil) } + +// Error 失败 +func Error(c *gin.Context) { + Send(c, http.StatusOK, FAIL, nil) +} + +// ErrorData 数据 +func ErrorData(c *gin.Context, data any) { + Send(c, http.StatusOK, FAIL, data) +} diff --git a/utils/validator.go b/utils/validator.go new file mode 100644 index 0000000..8d5cc69 --- /dev/null +++ b/utils/validator.go @@ -0,0 +1,53 @@ +package utils + +import ( + "reflect" + + "github.com/go-playground/locales/zh_Hans_CN" + unTrans "github.com/go-playground/universal-translator" + "github.com/go-playground/validator/v10" + zhTrans "github.com/go-playground/validator/v10/translations/zh" +) + +var Validator = new(ValidateUtil) + +type ValidateUtil struct{} + +var ( + uni *unTrans.UniversalTranslator + validate *validator.Validate +) + +// Validate 返回验证错误信息, 为 "" 则无错误 +func (v *ValidateUtil) Validate(data any) string { + // 验证对象 + validate = validator.New() + // 翻译器 + trans := v.validateTransInit(validate) + + err := validate.Struct(data) + if err != nil { + for _, e := range err.(validator.ValidationErrors) { + return e.Translate(trans) + } + } + + return "" +} + +// validateTransInit 数据验证翻译器 +func (*ValidateUtil) validateTransInit(validate *validator.Validate) unTrans.Translator { + // 万能翻译器,保存所有的语言环境和翻译数据 + uni = unTrans.New(zh_Hans_CN.New()) + // 翻译器 + trans, _ := uni.GetTranslator("zh_Hans_CN") + // 验证器注册翻译器 + _ = zhTrans.RegisterDefaultTranslations(validate, trans) + + // 读取 Tag 中的 label 标签为字段的翻译 + validate.RegisterTagNameFunc(func(field reflect.StructField) string { + return field.Tag.Get("label") + }) + + return trans +}