diff --git a/hunliji-api/hl-api b/hunliji-api/hl-api index d3b76c4..41ea73a 100644 Binary files a/hunliji-api/hl-api and b/hunliji-api/hl-api differ diff --git a/hunliji-api/visit/visit.go b/hunliji-api/visit/visit.go index ecf7b68..8f2a8ad 100644 --- a/hunliji-api/visit/visit.go +++ b/hunliji-api/visit/visit.go @@ -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 { diff --git a/vite-tailwindcss/src/views/index/index.vue b/vite-tailwindcss/src/views/index/index.vue index 07acf0f..ed1553e 100644 --- a/vite-tailwindcss/src/views/index/index.vue +++ b/vite-tailwindcss/src/views/index/index.vue @@ -647,8 +647,8 @@ const DEFAULTS = { introExitEffect: 'wind', } -// const data = ref({"date": "2026年9月4日 星期五", "bride": "李逸烁", "groom": "李琦", "hotel": "芙蓉酒楼太行店", "lunar": "农历七月廿三", "endImg": "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/132145000000000_1774761363179_compressed.webp", "address": "阳泉市芙蓉酒楼太行店 二楼", "heroImg": "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/130719000000000_1774761309141_compressed.webp", "musicUrl": "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3", "schedule": [{"desc": "Welcome Guests", "time": "11:00", "event": "签到迎宾"}, {"desc": "Wedding Ceremony", "time": "12:00", "event": "婚礼仪式"}, {"desc": "Wedding Banquet", "time": "12:30", "event": "敬备喜宴"}], "musicList": [{"url": "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3", "name": "背景音乐"}, {"url": "http://localhost:8080/uploads/1785462317_4112586452.mp3", "name": "梦中的婚礼"}, {"url": "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/165608000000000_M500001mh1Wp3Qq0BG.mp3", "name": "M500001mh1Wp3Qq0BG"}], "photoPages": [{"desc": "世界那么大,还是遇见了你。\n于千万人之中,没有早一步也没有晚一步。", "img1": "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/163856000000000_xwatermark.com_20260731163822453.jpg", "type": "single", "title": "初遇", "height": "80vh", "subtitle": "FIRST MET", "borderStyle": "gold", "singleWidth": "default", "singleKeepRatio": true}, {"desc": "纵然万劫不复,纵然相思入骨。\n我也待你眉眼如初,岁月如故。", "img1": "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/131028000000000_1774761252921.jpg", "img2": "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/131035000000000_1774761281171.jpg", "type": "overlap", "title": "相知", "height": "auto", "subtitle": "FALL IN LOVE", "borderStyle": "mat", "singleWidth": "default", "singleKeepRatio": true}, {"desc": "星光坠入眼眸,晚风轻抚过裙摆。\n浪漫不止于此,还有无数个明天。", "type": "one-large-five-small", "title": "相恋", "height": "80vh", "images": ["https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/131108000000000_1774761363179_compressed.webp", "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/131114000000000_1774761355188_compressed.webp", "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/131118000000000_1774761347929_compressed.webp", "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/131120000000000_1774761370955_compressed.webp", "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/131124000000000_1774761366112_compressed.webp", "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/131135000000000_1774761358034_compressed.webp"], "subtitle": "ROMANCE", "borderStyle": "mat", "singleWidth": "default", "singleKeepRatio": true}, {"desc": "往后余生,风雪是你,平淡是你。", "type": "nine-grid", "title": "相守", "height": "80vh", "images": ["https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/131539000000000_1774761212027_compressed.webp", "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/131544000000000_1774761239495_compressed.webp", "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/131555000000000_1774761227881_compressed.webp", "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/131551000000000_1774761286259_compressed.webp", "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/131612000000000_1774761242805_compressed.webp", "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/131605000000000_1774761217821_compressed.webp", "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/131627000000000_1774761237037_compressed.webp", "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/131646000000000_1774761259772_compressed.webp", "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/131631000000000_1774762692925_compressed.webp"], "subtitle": "FOREVER", "borderStyle": "mat", "singleWidth": "default", "singleKeepRatio": true}, {"desc": "如果您恰好有空,欢迎来见证幸福!", "img1": "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/132308000000000_1774761382424_compressed.webp", "type": "single", "title": "我们的婚礼", "height": "80vh", "subtitle": "Our wedding", "borderStyle": "none", "singleWidth": "full", "singleKeepRatio": true}, {"desc": "始于初见,止于终老,谨定于9月4日,盼与您分享我们的幸福时刻。", "img1": "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/132546000000000_1774761352228_compressed.webp", "type": "single", "title": "予君红妆,共赴白头", "height": "100vh", "subtitle": "Adorn you in crimson bridal grace, together we grow old, hand in hand.", "borderStyle": "none", "singleWidth": "full", "singleKeepRatio": true}, {"desc": "从乍见之欢到久处不厌,借此时光温润,定于X月X日,愿与您共饮一杯岁月的酒。", "img1": "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/132803000000000_1774761315900_compressed.webp", "type": "single", "title": "岁序温润,共饮此杯", "height": "100vh", "subtitle": "As the gentle year unfolds, let us share this cup.", "borderStyle": "none", "singleWidth": "full", "singleKeepRatio": true}, {"desc": "既许一人以偏爱,愿尽余生之慷慨;届时聊备喜筵,敬邀阖家光临。", "img1": "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/133128000000000_1774761336736_compressed.webp", "type": "single", "title": "三餐四季,有人共尝", "height": "100vh", "subtitle": "Three meals a day, four seasons a year—shared with someone dear.", "borderStyle": "none", "singleWidth": "full", "singleKeepRatio": true}], "scrollMode": "free", "formalText1": "源同一脉 庆衍二宗", "formalText2": "缔此良缘 匹配同称", "calendarDate": "2026-09-04", "introEnabled": true, "petalEnabled": true, "bubbleEnabled": true, "activeMusicUrl": "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/165608000000000_M500001mh1Wp3Qq0BG.mp3", "introExitEffect": "lift", "formalPageHeight": "60vh", "freeScrollInterval": 10, "pageSwitchDuration": 800, "firstScreenDuration": 1300}) -const data = ref({}) +const data = ref({"date": "2026年9月4日 星期五", "bride": "李逸烁", "groom": "李琦", "hotel": "芙蓉酒楼太行店", "lunar": "农历七月廿三", "endImg": "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/132145000000000_1774761363179_compressed.webp", "address": "阳泉市芙蓉酒楼太行店 二楼", "heroImg": "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/130719000000000_1774761309141_compressed.webp", "musicUrl": "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3", "schedule": [{"desc": "Welcome Guests", "time": "11:00", "event": "签到迎宾"}, {"desc": "Wedding Ceremony", "time": "12:00", "event": "婚礼仪式"}, {"desc": "Wedding Banquet", "time": "12:30", "event": "敬备喜宴"}], "musicList": [{"url": "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3", "name": "背景音乐"}, {"url": "http://localhost:8080/uploads/1785462317_4112586452.mp3", "name": "梦中的婚礼"}, {"url": "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/165608000000000_M500001mh1Wp3Qq0BG.mp3", "name": "M500001mh1Wp3Qq0BG"}], "photoPages": [{"desc": "世界那么大,还是遇见了你。\n于千万人之中,没有早一步也没有晚一步。", "img1": "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/163856000000000_xwatermark.com_20260731163822453.jpg", "type": "single", "title": "初遇", "height": "80vh", "subtitle": "FIRST MET", "borderStyle": "gold", "singleWidth": "default", "singleKeepRatio": true}, {"desc": "纵然万劫不复,纵然相思入骨。\n我也待你眉眼如初,岁月如故。", "img1": "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/131028000000000_1774761252921.jpg", "img2": "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/131035000000000_1774761281171.jpg", "type": "overlap", "title": "相知", "height": "auto", "subtitle": "FALL IN LOVE", "borderStyle": "mat", "singleWidth": "default", "singleKeepRatio": true}, {"desc": "星光坠入眼眸,晚风轻抚过裙摆。\n浪漫不止于此,还有无数个明天。", "type": "one-large-five-small", "title": "相恋", "height": "80vh", "images": ["https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/131108000000000_1774761363179_compressed.webp", "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/131114000000000_1774761355188_compressed.webp", "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/131118000000000_1774761347929_compressed.webp", "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/131120000000000_1774761370955_compressed.webp", "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/131124000000000_1774761366112_compressed.webp", "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/131135000000000_1774761358034_compressed.webp"], "subtitle": "ROMANCE", "borderStyle": "mat", "singleWidth": "default", "singleKeepRatio": true}, {"desc": "往后余生,风雪是你,平淡是你。", "type": "nine-grid", "title": "相守", "height": "80vh", "images": ["https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/131539000000000_1774761212027_compressed.webp", "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/131544000000000_1774761239495_compressed.webp", "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/131555000000000_1774761227881_compressed.webp", "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/131551000000000_1774761286259_compressed.webp", "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/131612000000000_1774761242805_compressed.webp", "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/131605000000000_1774761217821_compressed.webp", "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/131627000000000_1774761237037_compressed.webp", "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/131646000000000_1774761259772_compressed.webp", "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/131631000000000_1774762692925_compressed.webp"], "subtitle": "FOREVER", "borderStyle": "mat", "singleWidth": "default", "singleKeepRatio": true}, {"desc": "如果您恰好有空,欢迎来见证幸福!", "img1": "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/132308000000000_1774761382424_compressed.webp", "type": "single", "title": "我们的婚礼", "height": "80vh", "subtitle": "Our wedding", "borderStyle": "none", "singleWidth": "full", "singleKeepRatio": true}, {"desc": "始于初见,止于终老,谨定于9月4日,盼与您分享我们的幸福时刻。", "img1": "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/132546000000000_1774761352228_compressed.webp", "type": "single", "title": "予君红妆,共赴白头", "height": "100vh", "subtitle": "Adorn you in crimson bridal grace, together we grow old, hand in hand.", "borderStyle": "none", "singleWidth": "full", "singleKeepRatio": true}, {"desc": "从乍见之欢到久处不厌,借此时光温润,定于X月X日,愿与您共饮一杯岁月的酒。", "img1": "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/132803000000000_1774761315900_compressed.webp", "type": "single", "title": "岁序温润,共饮此杯", "height": "100vh", "subtitle": "As the gentle year unfolds, let us share this cup.", "borderStyle": "none", "singleWidth": "full", "singleKeepRatio": true}, {"desc": "既许一人以偏爱,愿尽余生之慷慨;届时聊备喜筵,敬邀阖家光临。", "img1": "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/133128000000000_1774761336736_compressed.webp", "type": "single", "title": "三餐四季,有人共尝", "height": "100vh", "subtitle": "Three meals a day, four seasons a year—shared with someone dear.", "borderStyle": "none", "singleWidth": "full", "singleKeepRatio": true}], "scrollMode": "free", "formalText1": "源同一脉 庆衍二宗", "formalText2": "缔此良缘 匹配同称", "calendarDate": "2026-09-04", "introEnabled": true, "petalEnabled": true, "bubbleEnabled": true, "activeMusicUrl": "https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/165608000000000_M500001mh1Wp3Qq0BG.mp3", "introExitEffect": "lift", "formalPageHeight": "60vh", "freeScrollInterval": 10, "pageSwitchDuration": 800, "firstScreenDuration": 1300}) +// const data = ref({}) const audio = useAudio() const isAutoScroll = ref(true), isDrawerOpen = ref(false), isSubmitted = ref(false)