7b6756f936
## 主要功能 新增离线「经纬度 → 美团 cityId」反查,让 `rec`(智能推荐)与 `top-sales`(销量最高)从离线库只返**同城券**(此前会混返异地券)。 - `app/utils/geo.py` + `app/utils/meituan_city.py`:坐标 → 美团 `city_id`(reverse_geocoder 离线反查,零网络)。 - `feed?tab=rec` / `/top-sales`:按 `city_id` 过滤;解析不出 / 老客户端不带坐标 → 降级返空。 - `top-sales` 与 `rec` 一致置空库内距离(相对城市默认点、对用户无意义)。 --------- Co-authored-by: guke <guke@autohome.com.cn> Reviewed-on: #116 Co-authored-by: guke <guke@wonderable.ai> Co-committed-by: guke <guke@wonderable.ai>
66 lines
3.1 KiB
Python
66 lines
3.1 KiB
Python
"""通过经纬度反查城市(reverse_geocoder 离线库,零网络调用)。
|
||
|
||
reverse_geocoder 内置 ~2.5M 条全球城市/聚居点的经纬度→地名映射表,
|
||
构建一次 KDTree(~几十MB 内存)后,查询为纯内存搜索,不作任何外部网络调用。
|
||
|
||
⚠️ 必须持有单例、且用 mode=1(单进程):
|
||
- reverse_geocoder 的模块级 rg.search()/rg.get() **每次调用都会 new 一个 RGeocoder**,
|
||
即每次都重新解析 ~2.5M 行 CSV + 重建 KDTree(数秒/次)。绝不能在服务端按请求调用。
|
||
- 默认 mode=2 用 multiprocessing 按 CPU 数 spawn 子进程做并行查询;在服务端 / Windows
|
||
spawn 下会重复 import 主模块(无 __main__ guard 时直接报错),既慢又危险。
|
||
故本模块持有一个 mode=1 的 RGeocoder 单例,建一次树、复用;查询走单进程内存搜索。
|
||
|
||
⚠️ 精度说明:gazetteer 里的中国数据粒度不一致——直辖市/省会通常直接命中城市名,
|
||
但部分城市会命中到区/街道级(如天津→Erwangzhuang、西安→Zhangjiabao),
|
||
此时 admin1(省级行政区)可作为回退。业务侧建议优先用 admin1 做城市级判定。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from typing import Any
|
||
|
||
import reverse_geocoder as rg # type: ignore[import-untyped]
|
||
|
||
# mode=1 单进程 KDTree 的单例;None 表示尚未构建(见 ensure_loaded)。
|
||
_geocoder: rg.RGeocoder | None = None
|
||
|
||
|
||
def ensure_loaded() -> None:
|
||
"""构建(或复用)RGeocoder 单例(幂等)。
|
||
|
||
首次调用解析 ~2.5M 行 CSV + 构建 KDTree(~秒级、数十 MB)。生产应在 main.py 的
|
||
lifespan 启动阶段主动调用一次,把这份一次性成本摊到启动,避免砸在第一个
|
||
/feed?tab=rec / /top-sales 请求上。mode=1 = 单进程,不 spawn 子进程。
|
||
"""
|
||
global _geocoder
|
||
if _geocoder is None:
|
||
_geocoder = rg.RGeocoder(mode=1, verbose=False)
|
||
|
||
|
||
def get_city(latitude: float, longitude: float) -> dict[str, str]:
|
||
"""根据经纬度反查最近聚居点。
|
||
|
||
返回 dict:
|
||
- name: 最近聚居点名称(英文),如 "Beijing" / "Fengsheng";
|
||
中国境内可能是区/街道级;海洋/无人区返 ""
|
||
- admin1: 省级行政区(英文),如 "Beijing" / "Hubei" / "Chongqing Shi";
|
||
直辖市 admin1 即为城市名
|
||
- country: ISO 3166-1 alpha-2,如 "CN"
|
||
- latitude: 匹配到的参考点纬度(字符串)
|
||
- longitude: 匹配到的参考点经度(字符串)
|
||
|
||
未匹配到(海洋/远洋)时返回空字符串字段。
|
||
"""
|
||
ensure_loaded()
|
||
assert _geocoder is not None # ensure_loaded 保证已构建
|
||
results: list[dict[str, Any]] = _geocoder.query([(latitude, longitude)])
|
||
if not results:
|
||
return {"name": "", "admin1": "", "country": "", "latitude": "", "longitude": ""}
|
||
r = results[0]
|
||
return {
|
||
"name": str(r.get("name", "")),
|
||
"admin1": str(r.get("admin1", "")),
|
||
"country": str(r.get("cc", "")),
|
||
"latitude": str(r.get("lat", "")),
|
||
"longitude": str(r.get("lon", "")),
|
||
}
|