153 lines
4.0 KiB
Go
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
|
|
}
|