feat(geoip): implement GeoIP functionality with initialization, IP geolocation retrieval, and unit tests

This commit is contained in:
keven1024
2026-05-23 21:19:12 +08:00
parent 3275b8f68f
commit 8828924a1f
5 changed files with 121 additions and 0 deletions

55
pkg/geoip/geoip.go Normal file
View File

@@ -0,0 +1,55 @@
package geoip
import (
"embed"
"net/netip"
"github.com/enescakir/emoji"
"github.com/oschwald/geoip2-golang/v2"
)
//go:embed resource/*.mmdb
var dbFS embed.FS
var geoipReader *geoip2.Reader
func Init() error {
data, err := dbFS.ReadFile("resource/GeoLite2-Country.mmdb")
if err != nil {
return err
}
geoipReader, err = geoip2.OpenBytes(data)
return err
}
type IpGeoInfo struct {
Country *geoip2.Country
Emoji string
}
func GetIpGeoInfo(ip string) *IpGeoInfo {
if geoipReader == nil || ip == "" {
return nil
}
ipAddr, err := netip.ParseAddr(ip)
if err != nil {
return nil
}
country, err := geoipReader.Country(ipAddr)
if err != nil || country.Country.ISOCode == "" {
return nil
}
emoji, err := emoji.CountryFlag(country.Country.ISOCode)
if err != nil {
return nil
}
return &IpGeoInfo{
Country: country,
Emoji: emoji.String(),
}
}

48
pkg/geoip/geoip_test.go Normal file
View File

@@ -0,0 +1,48 @@
package geoip
import (
"testing"
)
func initOrSkip(t *testing.T) {
t.Helper()
if err := Init(); err != nil {
t.Skipf("skipping: geoip database unavailable: %v", err)
}
}
func TestGetIpGeoInfo_Cloudflare(t *testing.T) {
initOrSkip(t)
ip := "223.167.150.96"
city := GetIpGeoInfo(ip)
if city == nil {
t.Fatalf("GetIpGeoInfo(%q) returned nil, expected a result", ip)
}
t.Logf("Country: %s (%s)", city.Country.Country.Names.English, city.Country.Country.ISOCode)
t.Logf("Emoji: %s", city.Emoji)
}
func TestGetIpGeoInfo_EmptyIP(t *testing.T) {
initOrSkip(t)
if city := GetIpGeoInfo(""); city != nil {
t.Errorf("GetIpGeoInfo(\"\") = %+v, want nil", city)
}
}
func TestGetIpGeoInfo_InvalidIP(t *testing.T) {
initOrSkip(t)
if city := GetIpGeoInfo("not-an-ip"); city != nil {
t.Errorf("GetIpGeoInfo(\"not-an-ip\") = %+v, want nil", city)
}
}
func TestGetIpGeoInfo_UninitializedReader(t *testing.T) {
geoipReader = nil
if city := GetIpGeoInfo("103.21.244.12"); city != nil {
t.Errorf("expected nil when reader is uninitialized, got %+v", city)
}
}

10
pkg/geoip/go.mod Normal file
View File

@@ -0,0 +1,10 @@
module pkg/geoip
go 1.25.5
require (
github.com/enescakir/emoji v1.0.0 // indirect
github.com/oschwald/geoip2-golang/v2 v2.1.0 // indirect
github.com/oschwald/maxminddb-golang/v2 v2.1.1 // indirect
golang.org/x/sys v0.38.0 // indirect
)

8
pkg/geoip/go.sum Normal file
View File

@@ -0,0 +1,8 @@
github.com/enescakir/emoji v1.0.0 h1:W+HsNql8swfCQFtioDGDHCHri8nudlK1n5p2rHCJoog=
github.com/enescakir/emoji v1.0.0/go.mod h1:Bt1EKuLnKDTYpLALApstIkAjdDrS/8IAgTkKp+WKFD0=
github.com/oschwald/geoip2-golang/v2 v2.1.0 h1:DjnLhNJu9WHwTrmoiQFvgmyJoczhdnm7LB23UBI2Amo=
github.com/oschwald/geoip2-golang/v2 v2.1.0/go.mod h1:qdVmcPgrTJ4q2eP9tHq/yldMTdp2VMr33uVdFbHBiBc=
github.com/oschwald/maxminddb-golang/v2 v2.1.1 h1:lA8FH0oOrM4u7mLvowq8IT6a3Q/qEnqRzLQn9eH5ojc=
github.com/oschwald/maxminddb-golang/v2 v2.1.1/go.mod h1:PLdx6PR+siSIoXqqy7C7r3SB3KZnhxWr1Dp6g0Hacl8=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=

Binary file not shown.