Files
past-go/past-go/internal/geo/geo.go
T

153 lines
4.0 KiB
Go

// Package geo 对应 index.php 顶部的腾讯位置服务函数:逆地理编码、IP 定位、球面距离。
package geo
import (
"encoding/json"
"fmt"
"io"
"math"
"net/http"
"net/url"
"strings"
"time"
"past/internal/store"
)
type Client struct {
Key string
http *http.Client
}
func New(key string) *Client {
return &Client{Key: key, http: &http.Client{Timeout: 5 * time.Second}}
}
func (c *Client) get(u string) map[string]any {
resp, err := c.http.Get(u)
if err != nil {
return nil
}
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
if len(b) == 0 {
return nil
}
var data map[string]any
if json.Unmarshal(b, &data) != nil {
return nil
}
return data
}
func statusOK(data map[string]any) bool {
if data == nil {
return false
}
st, ok := data["status"]
// PHP 用 === 0 判断:JSON 数字 0 解码为 float 0
return ok && store.ToFloat(st) == 0 && st != nil
}
func sub(m map[string]any, key string) map[string]any {
if m == nil {
return nil
}
v, _ := m[key].(map[string]any)
return v
}
// ReverseGeocode 经纬度(gcj02)→ "省 市 区县";未配置 key 或失败返回空串。
func (c *Client) ReverseGeocode(lat, lng float64) string {
if c.Key == "" || lat == 0 || lng == 0 {
return ""
}
u := fmt.Sprintf("https://apis.map.qq.com/ws/geocoder/v1/?location=%s,%s&key=%s&coord_type=5",
store.ToStr(lat), store.ToStr(lng), url.QueryEscape(c.Key))
data := c.get(u)
if !statusOK(data) {
return ""
}
result := sub(data, "result")
comp := sub(result, "address_component")
if comp == nil {
return ""
}
var parts []string
prov, city, dist := store.ToStr(comp["province"]), store.ToStr(comp["city"]), store.ToStr(comp["district"])
if prov != "" {
parts = append(parts, prov)
}
if city != "" && city != prov {
parts = append(parts, city)
}
if dist != "" {
parts = append(parts, dist)
}
if len(parts) > 0 {
return strings.Join(parts, " ")
}
return store.ToStr(result["address"])
}
// IPGeolocate IP → "市 区县 街道"(不含省份);失败返回空串。
func (c *Client) IPGeolocate(ip string) string {
if c.Key == "" || ip == "" || ip == "127.0.0.1" || ip == "::1" {
return ""
}
data := c.get("https://apis.map.qq.com/ws/location/v1/ip?ip=" + url.QueryEscape(ip) + "&key=" + url.QueryEscape(c.Key))
if !statusOK(data) {
return ""
}
result := sub(data, "result")
ad := sub(result, "ad_info")
if ad == nil {
return ""
}
if loc := sub(result, "location"); loc != nil && store.Truthy(loc["lat"]) && store.Truthy(loc["lng"]) {
geo := c.get(fmt.Sprintf("https://apis.map.qq.com/ws/geocoder/v1/?location=%s,%s&key=%s&coord_type=5",
store.ToStr(loc["lat"]), store.ToStr(loc["lng"]), url.QueryEscape(c.Key)))
if statusOK(geo) {
if comp := sub(sub(geo, "result"), "address_component"); comp != nil {
var parts []string
prov, city, dist, street := store.ToStr(comp["province"]), store.ToStr(comp["city"]), store.ToStr(comp["district"]), store.ToStr(comp["street"])
if city != "" && city != prov {
parts = append(parts, city)
}
if dist != "" {
parts = append(parts, dist)
}
if street != "" {
parts = append(parts, street)
}
if len(parts) > 0 {
return strings.Join(parts, " ")
}
}
}
}
var parts []string
prov, city, dist := store.ToStr(ad["province"]), store.ToStr(ad["city"]), store.ToStr(ad["district"])
if city != "" && city != prov {
parts = append(parts, city)
}
if dist != "" {
parts = append(parts, dist)
}
if len(parts) > 0 {
return strings.Join(parts, " ")
}
return store.ToStr(sub(result, "location")["city"])
}
// Haversine 两点球面距离(公里,保留 1 位小数)
func Haversine(lat1, lng1, lat2, lng2 float64) float64 {
const r = 6371.0
dLat := (lat2 - lat1) * math.Pi / 180
dLng := (lng2 - lng1) * math.Pi / 180
a := math.Sin(dLat/2)*math.Sin(dLat/2) +
math.Cos(lat1*math.Pi/180)*math.Cos(lat2*math.Pi/180)*math.Sin(dLng/2)*math.Sin(dLng/2)
c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
return math.Round(r*c*10) / 10
}