From e880dbe4589e677e98883859700873493c440b6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Tue, 15 Sep 2026 12:39:08 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20geo=20=E6=94=B6=E5=8F=A3=EF=BC=88?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E5=8D=95=E4=BE=8B/IPv6=E6=B5=8B=E8=AF=95/?= =?UTF-8?q?=E9=99=8D=E7=BA=A7=E6=97=A5=E5=BF=97=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 导出模块级 resolver 单例:Task 6 起 import resolver 使用, 防多实例各持 ~11MB xdb 副本、lru_cache 按实例分片 - 补公网 IPv6→None 测试,锁定 M1 v4-only 决策 - __init__ 降级路径(文件过短/结构无法识别/except)均记 warning 日志,运维可区分「没部署 xdb」与「库 API 崩了」 - 删掉测试未使用的 monkeypatch 参数 Co-Authored-By: Claude Code --- server/tracker/services/geo.py | 17 +++++++++++++++-- server/tracker/tests/test_geo.py | 8 +++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/server/tracker/services/geo.py b/server/tracker/services/geo.py index d509571..d8cdce2 100644 --- a/server/tracker/services/geo.py +++ b/server/tracker/services/geo.py @@ -1,5 +1,6 @@ import functools import ipaddress +import logging from django.conf import settings @@ -47,15 +48,22 @@ class GeoResolver: with open(settings.IP2REGION_XDB, "rb") as f: buff = f.read() if len(buff) < ip2region_util.HeaderInfoLength: - return # 文件过短,绝非合法 xdb + logging.getLogger(__name__).warning( + "GeoResolver 降级运行:xdb 文件过短(%d 字节)", len(buff)) + return header = ip2region_util.Header(buff[: ip2region_util.HeaderInfoLength]) version = ip2region_util.version_from_header(header) if version is None: - return # 无法识别的 xdb 结构/IP 版本 + logging.getLogger(__name__).warning( + "GeoResolver 降级运行:无法识别的 xdb 结构/IP 版本(header=%s)", header) + return # 结构版本同时决定数据段布局:2.0 旧布局 / 3.0 新布局 self._new_layout = header.version >= ip2region_util.XdbStructure30 self._searcher = ip2region_searcher.new_with_buffer(version, buff) except Exception: + # 运维可据此区分"没部署 xdb"与"库 API 崩了"(含堆栈) + logging.getLogger(__name__).warning( + "GeoResolver 降级运行(xdb 缺失或加载失败)", exc_info=True) self._searcher = None @functools.lru_cache(maxsize=10000) @@ -76,3 +84,8 @@ class GeoResolver: return parse_region(raw, self._new_layout) except Exception: return None + + +# 进程级共享单例:Task 6 起 from tracker.services.geo import resolver 使用, +# 勿再各自实例化 GeoResolver()——每个实例会把 ~11MB xdb 完整读入内存,且 lru_cache 按实例分片。 +resolver = GeoResolver() diff --git a/server/tracker/tests/test_geo.py b/server/tracker/tests/test_geo.py index 88e09e8..ca1dc77 100644 --- a/server/tracker/tests/test_geo.py +++ b/server/tracker/tests/test_geo.py @@ -14,13 +14,19 @@ def test_private_ip_returns_local(): assert r.resolve("127.0.0.1") is None -def test_public_ip_parsed_or_none(monkeypatch): +def test_public_ip_parsed_or_none(): r = GeoResolver() # xdb 未下载/解析失败时优雅降级返回 None,不抛异常 out = r.resolve("8.8.8.8") assert out is None or set(out) == {"country", "province", "city"} +def test_public_ipv6_returns_none(): + # 锁定 M1 的 v4-only 决策:公网 IPv6 查 v4 库因字节长度不符抛 ValueError, + # 降级为 None(防库/数据未来变更悄悄改变行为) + assert GeoResolver().resolve("2001:4860:4860::8888") is None + + def test_parse_region_two_layouts(): # 旧版 2.0 结构:"国家|区域|省|市|ISP" assert parse_region("中国|0|江苏省|南京市|电信", new_layout=False) == {