diff --git a/docs/superpowers/plans/2026-09-15-m1-data-collection-foundation.md b/docs/superpowers/plans/2026-09-15-m1-data-collection-foundation.md index 5857573..678affd 100644 --- a/docs/superpowers/plans/2026-09-15-m1-data-collection-foundation.md +++ b/docs/superpowers/plans/2026-09-15-m1-data-collection-foundation.md @@ -669,6 +669,7 @@ class GeoResolver: return None ``` 注:py-ip2region 的实际调用以其 README 为准(模块 `ip2region`,入口 `searcher.new_with_buffer(buff)` 后 `search(ip)`;**不支持 with 上下文管理器**——用错会被 except 静默吞掉、地理功能永久失效且测试照样绿,务必按此实现)。解析结果格式 `国家|区域|省|市|ISP`,如 `中国|0|广东省|深圳市|电信`、`美国|0|加利福尼亚|旧金山|0`。 +(**执行注记**:实际交付以提交为准——py-ip2region 3.0.4 `new_with_buffer(version, c_buffer)` 双参、现行 GitHub 库已改名 ip2region_v4.xdb 且五段布局为 `国家|省|城市|ISP|国家代码`(旧映射会把 ISP 串进 city),实现按 xdb 头版本分派新旧布局;xdb 外国条目为英文;"0" 为缺失占位(Task 6 写库时归一为空串);模块级导出 `resolver` 单例;**IPv6 M1 显示"未知"(v4-only 库,已接受)**;Task 7 的 `_client_ip` 需归一 `::ffff:` 前缀的 IPv4 映射地址防地理丢失。) - [ ] **Step 3: 跑测试通过**(`server/.gitignore` 加 `tracker/data/*.xdb`) @@ -749,9 +750,8 @@ from django.utils import timezone from tracker import constants as C from tracker.models import Visitor, VisitSession, Event from tracker.services.validation import validate_event -from tracker.services.geo import GeoResolver - -_geo = GeoResolver() +from tracker.services.geo import resolver as _geo # 模块级单例(勿再实例化,11MB/份) +# geo 返回值缺失段为 "0" 占位:写库前归一 x if x not in ("", "0") else "" _FIELD_LEN = {"device_type": 16, "os": 64, "browser": 64, "language": 32, "screen": 32} @@ -1034,9 +1034,11 @@ class CollectBatchView(APIView): def _client_ip(self, request): """nginx 前置时取 X-Forwarded-For 最后一段(与 DRF NUM_PROXIES=1 一致;套 CDN 时按层数前移)。""" xff = request.META.get("HTTP_X_FORWARDED_FOR", "") - if xff: - return xff.split(",")[-1].strip() - return request.META.get("REMOTE_ADDR") + ip = xff.split(",")[-1].strip() if xff else request.META.get("REMOTE_ADDR") + # 归一 IPv4 映射地址(::ffff:1.2.3.4 → 1.2.3.4),防地理解析丢失 + if ip and ip.lower().startswith("::ffff:"): + ip = ip[7:] + return ip def _deny(self, code, reason): resp = Response({"code": code, "data": None, "message": reason}, status=code)