feat: add Go backend
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
// Package location
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2024 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
package location
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gogf/gf/v2/container/gmap"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type IpCache struct {
|
||||
sync.Mutex
|
||||
data *gmap.Map
|
||||
}
|
||||
|
||||
var (
|
||||
cache = &IpCache{
|
||||
data: gmap.New(true),
|
||||
}
|
||||
)
|
||||
|
||||
func (c *IpCache) Contains(ip string) bool {
|
||||
return c.data.Contains(ip)
|
||||
}
|
||||
|
||||
func (c *IpCache) SetIpCache(ip string, data *IpLocationData) {
|
||||
if c.data.Size() > 10000 {
|
||||
c.data.Pops(2000)
|
||||
}
|
||||
c.data.Set(ip, data)
|
||||
}
|
||||
|
||||
func (c *IpCache) GetIpCache(ip string) (*IpLocationData, error) {
|
||||
value := c.data.Get(ip)
|
||||
data1, ok := value.(*IpLocationData)
|
||||
if !ok {
|
||||
c.data.Remove(ip)
|
||||
err := fmt.Errorf("data assertion failed in the cache ip:%v", ip)
|
||||
return nil, err
|
||||
}
|
||||
return data1, nil
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// Package location
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2023 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
package location
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type AMapGeocodeAddressRes struct {
|
||||
Status string `json:"status"`
|
||||
Info string `json:"info"`
|
||||
Infocode string `json:"infocode"`
|
||||
Count string `json:"count"`
|
||||
Geocodes []struct {
|
||||
FormattedAddress string `json:"formatted_address"`
|
||||
Country string `json:"country"`
|
||||
Province string `json:"province"`
|
||||
Citycode string `json:"citycode"`
|
||||
City string `json:"city"`
|
||||
District string `json:"district"`
|
||||
Township []interface{} `json:"township"`
|
||||
Neighborhood struct {
|
||||
Name []interface{} `json:"name"`
|
||||
Type []interface{} `json:"type"`
|
||||
} `json:"neighborhood"`
|
||||
Building struct {
|
||||
Name []interface{} `json:"name"`
|
||||
Type []interface{} `json:"type"`
|
||||
} `json:"building"`
|
||||
Adcode string `json:"adcode"`
|
||||
Street []interface{} `json:"street"`
|
||||
Number []interface{} `json:"number"`
|
||||
Location string `json:"location"`
|
||||
Level string `json:"level"`
|
||||
} `json:"geocodes"`
|
||||
}
|
||||
|
||||
type AddressRegion struct {
|
||||
ProvinceName string `json:"provinceName"`
|
||||
CityName string `json:"cityName"`
|
||||
CountyName string `json:"countyName"`
|
||||
ProvinceCode string `json:"provinceCode"`
|
||||
CityCode string `json:"cityCode"`
|
||||
CountyCode string `json:"countyCode"`
|
||||
}
|
||||
|
||||
// ParseAddress 将地址解析出省市区编码
|
||||
func ParseAddress(ctx context.Context, address, key string) (region *AddressRegion, err error) {
|
||||
var (
|
||||
url = fmt.Sprintf("https://restapi.amap.com/v3/geocode/geo?address=%v&output=JSON&key=%v", address, key)
|
||||
responseMap = make(g.Map)
|
||||
response *AMapGeocodeAddressRes
|
||||
)
|
||||
|
||||
err = g.Client().GetVar(ctx, url).Scan(&responseMap)
|
||||
if err != nil {
|
||||
return nil, gerror.Newf("AMap AnalysisAddress err:%+v", err)
|
||||
}
|
||||
|
||||
err = gconv.Scan(responseMap, &response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 异常状态码
|
||||
if response.Status != "1" {
|
||||
return nil, gerror.Newf("AMap AnalysisAddress 错误码:%+v, 错误提示:%+v", response.Status, response.Info)
|
||||
}
|
||||
|
||||
if len(response.Geocodes) == 0 {
|
||||
return nil, gerror.New("AMap AnalysisAddress 没有解析到地区信息")
|
||||
}
|
||||
|
||||
region = new(AddressRegion)
|
||||
region.ProvinceName = response.Geocodes[0].Province
|
||||
region.CityName = response.Geocodes[0].City
|
||||
region.CountyName = response.Geocodes[0].District
|
||||
|
||||
// 有效区域编码
|
||||
if len(response.Geocodes[0].Adcode) == 6 {
|
||||
code := gconv.Int64(response.Geocodes[0].Adcode)
|
||||
if code > 0 {
|
||||
region.ProvinceCode = gconv.String(code / 10000 * 10000)
|
||||
region.CityCode = gconv.String(code / 100 * 100)
|
||||
region.CountyCode = response.Geocodes[0].Adcode
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
// Package location
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2023 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
package location
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"hotgo/utility/validate"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/encoding/gcharset"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
"github.com/kayon/iploc"
|
||||
)
|
||||
|
||||
const (
|
||||
whoisApi = "https://whois.pconline.com.cn/ipJson.jsp?json=true&ip="
|
||||
dyndns = "http://members.3322.org/dyndns/getip" // 备用:"https://ifconfig.co/ip"
|
||||
)
|
||||
|
||||
type IpLocationData struct {
|
||||
Ip string `json:"ip"`
|
||||
Country string `json:"country"`
|
||||
Region string `json:"region"`
|
||||
Province string `json:"province"`
|
||||
ProvinceCode int64 `json:"province_code"`
|
||||
City string `json:"city"`
|
||||
CityCode int64 `json:"city_code"`
|
||||
Area string `json:"area"`
|
||||
AreaCode int64 `json:"area_code"`
|
||||
}
|
||||
|
||||
type WhoisRegionData struct {
|
||||
Ip string `json:"ip"`
|
||||
Pro string `json:"pro" `
|
||||
ProCode string `json:"proCode" `
|
||||
City string `json:"city" `
|
||||
CityCode string `json:"cityCode"`
|
||||
Region string `json:"region"`
|
||||
RegionCode string `json:"regionCode"`
|
||||
Addr string `json:"addr"`
|
||||
Err string `json:"err"`
|
||||
}
|
||||
|
||||
var (
|
||||
defaultRetry int64 = 3 // 默认重试次数
|
||||
)
|
||||
|
||||
// WhoisLocation 通过Whois接口查询IP归属地
|
||||
func WhoisLocation(ctx context.Context, ip string, retry ...int64) (*IpLocationData, error) {
|
||||
response, err := g.Client().Timeout(10*time.Second).Get(ctx, whoisApi+ip)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer response.Close()
|
||||
|
||||
str, err := gcharset.ToUTF8("GBK", response.ReadAllString())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 利用重试机制缓解高并发情况下限流的影响
|
||||
// 毕竟这是一个免费的接口,如果你对IP归属地定位要求毕竟高,可以考虑换个付费接口
|
||||
if response.StatusCode != http.StatusOK {
|
||||
retryCount := defaultRetry
|
||||
if len(retry) > 0 {
|
||||
retryCount = retry[0]
|
||||
}
|
||||
if retryCount > 0 {
|
||||
retryCount--
|
||||
return WhoisLocation(ctx, ip, retryCount)
|
||||
}
|
||||
}
|
||||
|
||||
var who *WhoisRegionData
|
||||
if err = gconv.Scan([]byte(str), &who); err != nil {
|
||||
err = gerror.Newf("WhoisLocation Scan err:%v, str:%v", err, str)
|
||||
return nil, err
|
||||
}
|
||||
return &IpLocationData{
|
||||
Ip: who.Ip,
|
||||
Region: who.Addr,
|
||||
Province: who.Pro,
|
||||
ProvinceCode: gconv.Int64(who.ProCode),
|
||||
City: who.City,
|
||||
CityCode: gconv.Int64(who.CityCode),
|
||||
Area: who.Region,
|
||||
AreaCode: gconv.Int64(who.RegionCode),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Cz88Find 通过Cz88的IP库查询IP归属地
|
||||
func Cz88Find(ctx context.Context, ip string) (*IpLocationData, error) {
|
||||
loc, err := iploc.OpenWithoutIndexes("./resource/ip/qqwry-utf8.dat")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%v for help, please go to: https://github.com/kayon/iploc", err.Error())
|
||||
}
|
||||
|
||||
detail := loc.Find(ip)
|
||||
if detail == nil {
|
||||
return nil, fmt.Errorf("no ip data is queried. procedure:%v", ip)
|
||||
}
|
||||
return &IpLocationData{
|
||||
Ip: ip,
|
||||
Country: detail.Country,
|
||||
Region: detail.Region,
|
||||
Province: detail.Province,
|
||||
City: detail.City,
|
||||
Area: detail.County,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetLocation 获取IP归属地信息
|
||||
func GetLocation(ctx context.Context, ip string) (data *IpLocationData, err error) {
|
||||
if !validate.IsIp(ip) {
|
||||
return nil, fmt.Errorf("invalid input ip:%v", ip)
|
||||
}
|
||||
|
||||
if validate.IsLocalIPAddr(ip) {
|
||||
return // nil, fmt.Errorf("must be a public ip:%v", ip)
|
||||
}
|
||||
|
||||
if cache.Contains(ip) {
|
||||
return cache.GetIpCache(ip)
|
||||
}
|
||||
|
||||
cache.Lock()
|
||||
defer cache.Unlock()
|
||||
|
||||
if cache.Contains(ip) {
|
||||
return cache.GetIpCache(ip)
|
||||
}
|
||||
|
||||
mode := g.Cfg().MustGet(ctx, "system.ipMethod", "cz88").String()
|
||||
switch mode {
|
||||
case "whois":
|
||||
data, err = WhoisLocation(ctx, ip)
|
||||
default:
|
||||
data, err = Cz88Find(ctx, ip)
|
||||
}
|
||||
|
||||
if err == nil && data != nil {
|
||||
cache.SetIpCache(ip, data)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetPublicIP 获取公网IP
|
||||
func GetPublicIP(ctx context.Context) (ip string, err error) {
|
||||
var data *WhoisRegionData
|
||||
err = g.Client().Timeout(10*time.Second).GetVar(ctx, whoisApi).Scan(&data)
|
||||
if err != nil {
|
||||
g.Log().Info(ctx, "GetPublicIP fail, alternatives are being tried.")
|
||||
return GetPublicIP2()
|
||||
}
|
||||
|
||||
if data == nil {
|
||||
g.Log().Info(ctx, "publicIP address Parsing failure, check the network and firewall blocking.")
|
||||
return "0.0.0.0", nil
|
||||
}
|
||||
return data.Ip, nil
|
||||
}
|
||||
|
||||
func GetPublicIP2() (ip string, err error) {
|
||||
response, err := http.Get(dyndns)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ip = strings.ReplaceAll(string(body), "\n", "")
|
||||
return
|
||||
}
|
||||
|
||||
// GetLocalIP 获取服务器内网IP
|
||||
func GetLocalIP() (ip string, err error) {
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
ipAddr, ok := addr.(*net.IPNet)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if ipAddr.IP.IsLoopback() {
|
||||
continue
|
||||
}
|
||||
if !ipAddr.IP.IsGlobalUnicast() {
|
||||
continue
|
||||
}
|
||||
return ipAddr.IP.String(), nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetClientIp 获取客户端IP
|
||||
func GetClientIp(r *ghttp.Request) string {
|
||||
if r == nil {
|
||||
return ""
|
||||
}
|
||||
ip := r.Header.Get("X-Forwarded-For")
|
||||
if ip == "" {
|
||||
ip = r.GetClientIp()
|
||||
}
|
||||
|
||||
// 兼容部分云厂商CDN,如果存在多个,默认取第一个
|
||||
if gstr.Contains(ip, ",") {
|
||||
ip = gstr.StrTillEx(ip, ",")
|
||||
}
|
||||
|
||||
if gstr.Contains(ip, ", ") {
|
||||
ip = gstr.StrTillEx(ip, ", ")
|
||||
}
|
||||
return ip
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// Package location_test
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2024 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
package location_test
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/test/gtest"
|
||||
"hotgo/internal/library/location"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var ip = "120.12.151.65"
|
||||
|
||||
func Test_GetLocation(t *testing.T) {
|
||||
ctx := gctx.New()
|
||||
data, err := location.GetLocation(ctx, ip)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
t.Logf("data:%v", gjson.New(data).String())
|
||||
}
|
||||
|
||||
func Test_ParallelGetLocation(t *testing.T) {
|
||||
ctx := gctx.New()
|
||||
start := gtime.Now()
|
||||
|
||||
t.Log("start")
|
||||
for i := 0; i < 10; i++ {
|
||||
data, err := location.GetLocation(ctx, ip)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
t.Logf("index:%v, data:%v", i, gjson.New(data).String())
|
||||
}
|
||||
|
||||
t.Logf("总耗时:%vs", gtime.Now().Sub(start).Seconds())
|
||||
}
|
||||
|
||||
func Test_ConcurrentGetLocation(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
ctx := gctx.New()
|
||||
start := gtime.Now()
|
||||
var wg sync.WaitGroup
|
||||
|
||||
t.Log("start")
|
||||
for i := 0; i < 10; i++ {
|
||||
wg.Add(1)
|
||||
index := i
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
data, err := location.GetLocation(ctx, ip)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
t.Logf("index:%v, data:%v", index, gjson.New(data).String())
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
t.Logf("总耗时:%vs", gtime.Now().Sub(start).Seconds())
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
// Package location
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2023 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
package location
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/dao"
|
||||
"hotgo/internal/model/entity"
|
||||
"hotgo/utility/tree"
|
||||
|
||||
"github.com/gogf/gf/v2/container/gvar"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
func GetModel(ctx context.Context) *gdb.Model {
|
||||
return dao.SysProvinces.Ctx(ctx)
|
||||
}
|
||||
|
||||
// ParseSimpleRegion 通过地区ID解析地区名称,自动加入上级地区
|
||||
func ParseSimpleRegion(ctx context.Context, id int64, spilt ...string) (string, error) {
|
||||
if id == 0 {
|
||||
return "", nil
|
||||
}
|
||||
var (
|
||||
models *entity.SysProvinces
|
||||
err error
|
||||
)
|
||||
|
||||
if err = GetModel(ctx).Fields(dao.SysProvinces.Columns().Title, dao.SysProvinces.Columns().Level, dao.SysProvinces.Columns().Tree).Where(dao.SysProvinces.Columns().Id, id).Scan(&models); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if models == nil {
|
||||
return "", gerror.Newf("the area code :%v is not in the database", id)
|
||||
}
|
||||
|
||||
if models.Level == 1 {
|
||||
return models.Title, nil
|
||||
}
|
||||
|
||||
ids := tree.GetIds(models.Tree)
|
||||
|
||||
if models.Level == 2 {
|
||||
if len(ids) != 1 {
|
||||
return "", gerror.Newf("the region code is incorrectly configured, models:%+v, ids:%v", models, ids)
|
||||
}
|
||||
return ParseRegion(ctx, ids[0], id, 0, spilt...)
|
||||
}
|
||||
|
||||
if models.Level == 3 {
|
||||
if len(ids) != 2 {
|
||||
return "", gerror.Newf("the region code is incorrectly configured, models:%+v, ids:%v", models, ids)
|
||||
}
|
||||
return ParseRegion(ctx, ids[0], ids[1], id, spilt...)
|
||||
}
|
||||
return "", gerror.New("currently, it is only supported to regional areas")
|
||||
}
|
||||
|
||||
// ParseRegion 解析省市编码对应的地区名称
|
||||
func ParseRegion(ctx context.Context, province int64, city int64, county int64, spilt ...string) (string, error) {
|
||||
var (
|
||||
provinceName *gvar.Var
|
||||
cityName *gvar.Var
|
||||
countyName *gvar.Var
|
||||
err error
|
||||
)
|
||||
|
||||
// 分隔符
|
||||
sp := consts.RegionSpilt
|
||||
if len(spilt) > 0 {
|
||||
sp = spilt[0]
|
||||
}
|
||||
|
||||
if province > 0 && province < 999999 {
|
||||
provinceName, err = GetModel(ctx).Where(dao.SysProvinces.Columns().Id, province).Fields(dao.SysProvinces.Columns().Title).Value()
|
||||
if err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return "", err
|
||||
}
|
||||
|
||||
if city > 0 {
|
||||
cityName, err = GetModel(ctx).Where(dao.SysProvinces.Columns().Id, city).Fields(dao.SysProvinces.Columns().Title).Value()
|
||||
if err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
if county > 0 {
|
||||
countyName, err = GetModel(ctx).Where(dao.SysProvinces.Columns().Id, county).Fields(dao.SysProvinces.Columns().Title).Value()
|
||||
if err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return "保留地址", nil
|
||||
}
|
||||
|
||||
if province > 0 && city > 0 && county > 0 {
|
||||
return provinceName.String() + sp + cityName.String() + sp + countyName.String(), nil
|
||||
}
|
||||
|
||||
if province > 0 && city > 0 {
|
||||
return provinceName.String() + sp + cityName.String(), nil
|
||||
}
|
||||
return provinceName.String(), nil
|
||||
}
|
||||
Reference in New Issue
Block a user