1. 优化视觉效果、后端规范化

This commit is contained in:
李琦
2026-08-02 19:12:24 +08:00
parent 2a4d2e0bea
commit 405ef6bd52
3 changed files with 109 additions and 31 deletions

Binary file not shown.

View File

@@ -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