1. 优化视觉效果、后端规范化
This commit is contained in:
Binary file not shown.
@@ -61,16 +61,25 @@ type ipCacheEntry struct {
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
const (
|
||||
ipGeoBaseURL = "http://ip.nailaoyun.cn/"
|
||||
// 与 PHP 侧 app-key 一致
|
||||
ipGeoAppKey = "aff324073c3d236da5fff61aafd8b15bVTJGc2RHVmtYMSsyaUVIZzAxTkYxRnluN3lvcDVyQlJKSTljVnMvOC9iTT0="
|
||||
)
|
||||
|
||||
var (
|
||||
ipGeoCache = map[string]ipCacheEntry{}
|
||||
ipGeoCacheMu sync.Mutex
|
||||
ipHTTPClient = &http.Client{Timeout: 2 * time.Second}
|
||||
ipHTTPClient = &http.Client{Timeout: 3 * time.Second}
|
||||
)
|
||||
|
||||
func lookupIPGeo(ip string) ipGeoResult {
|
||||
ip = strings.TrimSpace(ip)
|
||||
if ip == "" || ip == "127.0.0.1" || ip == "::1" {
|
||||
return ipGeoResult{Raw: "本地"}
|
||||
if ip == "" {
|
||||
return ipGeoResult{Raw: "未知"}
|
||||
}
|
||||
if ip == "127.0.0.1" || ip == "::1" || strings.HasPrefix(ip, "192.168.") || strings.HasPrefix(ip, "10.") {
|
||||
return ipGeoResult{Raw: "本地", Region: "本地"}
|
||||
}
|
||||
|
||||
ipGeoCacheMu.Lock()
|
||||
@@ -89,48 +98,93 @@ func lookupIPGeo(ip string) ipGeoResult {
|
||||
}
|
||||
|
||||
func fetchIPGeo(ip string) ipGeoResult {
|
||||
body, _ := json.Marshal(map[string]string{"ip": ip})
|
||||
req, err := http.NewRequest(http.MethodPost, "http://ip.nailaoyun.cn/api/search-ip", bytes.NewReader(body))
|
||||
payload, _ := json.Marshal(map[string]interface{}{
|
||||
"ip": ip,
|
||||
"type": 2,
|
||||
})
|
||||
req, err := http.NewRequest(http.MethodPost, ipGeoBaseURL+"api/search-ip", bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return ipGeoResult{}
|
||||
return ipGeoResult{Raw: "未知"}
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("app-key", ipGeoAppKey)
|
||||
resp, err := ipHTTPClient.Do(req)
|
||||
if err != nil {
|
||||
log.Printf("IP归属地查询失败 %s: %v", ip, err)
|
||||
return ipGeoResult{}
|
||||
return ipGeoResult{Raw: "未知"}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
raw, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
if err != nil || len(raw) == 0 {
|
||||
return ipGeoResult{}
|
||||
return ipGeoResult{Raw: "未知"}
|
||||
}
|
||||
return parseIPGeoJSON(raw)
|
||||
}
|
||||
|
||||
func ipGeoCodeFailed(code interface{}) bool {
|
||||
switch v := code.(type) {
|
||||
case nil:
|
||||
return false
|
||||
case bool:
|
||||
return v
|
||||
case float64:
|
||||
return v != 0
|
||||
case int:
|
||||
return v != 0
|
||||
case int64:
|
||||
return v != 0
|
||||
case string:
|
||||
s := strings.TrimSpace(v)
|
||||
return s != "" && s != "0" && !strings.EqualFold(s, "false") && !strings.EqualFold(s, "ok") && !strings.EqualFold(s, "success")
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func asString(v interface{}) string {
|
||||
switch t := v.(type) {
|
||||
case string:
|
||||
return strings.TrimSpace(t)
|
||||
case float64:
|
||||
return strconv.FormatInt(int64(t), 10)
|
||||
case json.Number:
|
||||
return t.String()
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func parseIPGeoJSON(raw []byte) ipGeoResult {
|
||||
var root map[string]interface{}
|
||||
if err := json.Unmarshal(raw, &root); err != nil {
|
||||
return ipGeoResult{}
|
||||
return ipGeoResult{Raw: "未知"}
|
||||
}
|
||||
|
||||
// PHP: if ($result['body']['code']) return 未知
|
||||
if ipGeoCodeFailed(root["code"]) {
|
||||
return ipGeoResult{Raw: "未知"}
|
||||
}
|
||||
|
||||
// 成功时 PHP 直接返回 body;归属地主要在 result 字段
|
||||
resultStr := asString(root["result"])
|
||||
|
||||
data := root
|
||||
if nested, ok := root["data"].(map[string]interface{}); ok {
|
||||
data = nested
|
||||
if resultStr == "" {
|
||||
resultStr = asString(nested["result"])
|
||||
}
|
||||
} else if nested, ok := root["result"].(map[string]interface{}); ok {
|
||||
data = nested
|
||||
if resultStr == "" {
|
||||
resultStr = asString(nested["result"])
|
||||
}
|
||||
}
|
||||
|
||||
pick := func(keys ...string) string {
|
||||
for _, k := range keys {
|
||||
if v, ok := data[k]; ok {
|
||||
switch t := v.(type) {
|
||||
case string:
|
||||
if s := strings.TrimSpace(t); s != "" {
|
||||
return s
|
||||
}
|
||||
case float64:
|
||||
return strconv.FormatInt(int64(t), 10)
|
||||
}
|
||||
if s := asString(data[k]); s != "" {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return ""
|
||||
@@ -140,20 +194,28 @@ func parseIPGeoJSON(raw []byte) ipGeoResult {
|
||||
region := pick("province", "region", "Region", "Province", "state")
|
||||
city := pick("city", "City")
|
||||
isp := pick("isp", "ISP", "operator", "org")
|
||||
addr := pick("addr", "address", "location", "Location", "area")
|
||||
|
||||
parts := []string{}
|
||||
for _, p := range []string{country, region, city} {
|
||||
if p != "" && (len(parts) == 0 || parts[len(parts)-1] != p) {
|
||||
parts = append(parts, p)
|
||||
}
|
||||
}
|
||||
rawLoc := strings.Join(parts, " ")
|
||||
// result 常为完整文案,如「中国 广东省 深圳市 电信」
|
||||
rawLoc := resultStr
|
||||
if rawLoc == "" {
|
||||
rawLoc = addr
|
||||
parts := []string{}
|
||||
for _, p := range []string{country, region, city} {
|
||||
if p != "" && (len(parts) == 0 || parts[len(parts)-1] != p) {
|
||||
parts = append(parts, p)
|
||||
}
|
||||
}
|
||||
rawLoc = strings.Join(parts, " ")
|
||||
}
|
||||
if rawLoc == "" && isp != "" {
|
||||
rawLoc = isp
|
||||
if rawLoc == "" {
|
||||
rawLoc = pick("addr", "address", "location", "Location", "area")
|
||||
}
|
||||
if rawLoc == "" {
|
||||
rawLoc = "未知"
|
||||
}
|
||||
|
||||
// 若只有完整文案,尽量拆出省用于统计
|
||||
if region == "" && rawLoc != "" && rawLoc != "未知" && rawLoc != "本地" {
|
||||
region = guessProvince(rawLoc)
|
||||
}
|
||||
|
||||
return ipGeoResult{
|
||||
@@ -165,6 +227,22 @@ func parseIPGeoJSON(raw []byte) ipGeoResult {
|
||||
}
|
||||
}
|
||||
|
||||
func guessProvince(raw string) string {
|
||||
// 粗略从完整归属地文案中取省级片段,供地区分布统计
|
||||
tokens := strings.Fields(strings.ReplaceAll(raw, " ", " "))
|
||||
for _, t := range tokens {
|
||||
if strings.Contains(t, "省") || strings.Contains(t, "自治区") ||
|
||||
strings.HasSuffix(t, "市") && (strings.HasPrefix(t, "北京") || strings.HasPrefix(t, "上海") ||
|
||||
strings.HasPrefix(t, "天津") || strings.HasPrefix(t, "重庆")) {
|
||||
return t
|
||||
}
|
||||
}
|
||||
if len(tokens) >= 2 {
|
||||
return tokens[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func truncateRunes(s string, max int) string {
|
||||
r := []rune(s)
|
||||
if len(r) <= max {
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user