feat: fetcher 支援多 target union 掃描
- 新增 RADAR_TARGETS 環境變數('name:lat,lng;...') - 未設時 fall back 到單點 RADAR_LAT/LNG(backward-compat) - 主流程改為 loop 每個 target 呼叫 Function Server,以 (id, lat, lng, expire_time) dedupe 後 union - 單點失敗不阻斷其他 target(try/except 包起來) - debug 記錄每 target 原始筆數與完整 targets 清單 - 搭配 RADAR_ZOOM=15 解決 zoom 11 廣視角下單位面積密度過低的問題 本機驗證:5 target(台北/內湖/板橋/新竹/宜蘭)union 191 筆, twpk 內湖南港街區 bounds(zoom 17 視角)從 0 筆變 2 筆。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -29,7 +29,10 @@ UA_FROM_STATE = os.getenv("UA_FROM_STATE", "true").lower() in ("1", "true", "yes
|
||||
RADAR_LAT = float(os.getenv("RADAR_LAT", "25.0478"))
|
||||
RADAR_LNG = float(os.getenv("RADAR_LNG", "121.5170"))
|
||||
|
||||
# ✅ 對齊 fetch_and_upload.sh:ZOOM=11
|
||||
# 多點掃描:'name:lat,lng;name2:lat2,lng2;...'。未設時 fall back 到 RADAR_LAT/LNG 單點
|
||||
RADAR_TARGETS = os.getenv("RADAR_TARGETS", "").strip()
|
||||
|
||||
# ✅ 對齊 fetch_and_upload.sh:ZOOM=11(多點模式建議改 15)
|
||||
RADAR_ZOOM = int(os.getenv("RADAR_ZOOM", "11"))
|
||||
|
||||
RADAR_SHOT = os.getenv("RADAR_SHOT", "false").lower() in ("1", "true", "yes", "y")
|
||||
@@ -223,6 +226,26 @@ def _postprocess(
|
||||
return out
|
||||
|
||||
|
||||
def _parse_targets() -> List[tuple]:
|
||||
"""解析 RADAR_TARGETS;未設時 fall back 單點 RADAR_LAT/LNG。
|
||||
格式:'name:lat,lng;name2:lat2,lng2;...'
|
||||
"""
|
||||
if not RADAR_TARGETS:
|
||||
return [("default", RADAR_LAT, RADAR_LNG)]
|
||||
targets = []
|
||||
for chunk in RADAR_TARGETS.split(";"):
|
||||
chunk = chunk.strip()
|
||||
if not chunk:
|
||||
continue
|
||||
name, _, coord = chunk.partition(":")
|
||||
lat_s, _, lng_s = coord.partition(",")
|
||||
try:
|
||||
targets.append((name.strip(), float(lat_s), float(lng_s)))
|
||||
except ValueError:
|
||||
logger.warning(f"skip malformed RADAR_TARGETS entry: {chunk}")
|
||||
return targets or [("default", RADAR_LAT, RADAR_LNG)]
|
||||
|
||||
|
||||
def _cache_to_redis(payload: Dict[str, Any]) -> None:
|
||||
r = redis.Redis(
|
||||
host=REDIS_HOST,
|
||||
@@ -240,27 +263,38 @@ def _cache_to_redis(payload: Dict[str, Any]) -> None:
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
targets = _parse_targets()
|
||||
logger.info(
|
||||
"ENV: RADAR_ZOOM={}, MIN_REMAINING={}, PERFECT_ONLY={}, UA_FROM_STATE={}, SCRIPT_PATH={}, STATE_PATH={}".format(
|
||||
os.getenv("RADAR_ZOOM"),
|
||||
os.getenv("MIN_REMAINING"),
|
||||
os.getenv("PERFECT_ONLY"),
|
||||
os.getenv("UA_FROM_STATE"),
|
||||
SCRIPT_PATH,
|
||||
STATE_PATH,
|
||||
)
|
||||
)
|
||||
"ENV: RADAR_ZOOM={}, MIN_REMAINING={}, PERFECT_ONLY={}, UA_FROM_STATE={}, targets={}".format(
|
||||
RADAR_ZOOM, MIN_REMAINING, PERFECT_ONLY, UA_FROM_STATE, len(targets),
|
||||
)
|
||||
)
|
||||
|
||||
script_text = _load_script_text(SCRIPT_PATH)
|
||||
state_injected = _inject_state(script_text, STATE_PATH, UA_FROM_STATE)
|
||||
|
||||
code = _load_script_text(SCRIPT_PATH)
|
||||
code = _inject_target_and_shot(code, RADAR_LAT, RADAR_LNG, RADAR_ZOOM, RADAR_SHOT)
|
||||
code = _inject_state(code, STATE_PATH, UA_FROM_STATE)
|
||||
all_raw: Dict[Any, Dict[str, Any]] = {} # dedupe by (id, lat, lng, expire_time)
|
||||
merged_notes: List[str] = []
|
||||
target_stats: List[str] = []
|
||||
|
||||
parsed = _call_function_server(code)
|
||||
raw_items = parsed.get("results", [])
|
||||
debug = parsed.get("debug", {})
|
||||
for i, (name, lat, lng) in enumerate(targets, 1):
|
||||
logger.info(f"[{i}/{len(targets)}] fetching '{name}' at ({lat},{lng}) zoom={RADAR_ZOOM}")
|
||||
try:
|
||||
code = _inject_target_and_shot(state_injected, lat, lng, RADAR_ZOOM, RADAR_SHOT)
|
||||
parsed = _call_function_server(code)
|
||||
raw_items = parsed.get("results", [])
|
||||
target_stats.append(f"{name}:{len(raw_items)}")
|
||||
for r in raw_items:
|
||||
key = (r.get("id"), r.get("latitude"), r.get("longitude"), r.get("expire_time"))
|
||||
all_raw.setdefault(key, r)
|
||||
notes = parsed.get("debug", {}).get("notes", []) if isinstance(parsed.get("debug"), dict) else []
|
||||
merged_notes.extend(f"[{name}] {n}" for n in notes)
|
||||
except Exception as e:
|
||||
logger.exception(f"[{i}/{len(targets)}] '{name}' failed: {e}")
|
||||
target_stats.append(f"{name}:ERROR")
|
||||
|
||||
logger.info(f"Got {len(raw_items)} results (before filters)")
|
||||
raw_items = list(all_raw.values())
|
||||
logger.info(f"union total {len(raw_items)} unique (per-target: {', '.join(target_stats)})")
|
||||
|
||||
filtered = _postprocess(
|
||||
raw_items,
|
||||
@@ -270,7 +304,6 @@ def main() -> int:
|
||||
species=SPECIES,
|
||||
)
|
||||
|
||||
# ✅ filtered=0 時印出原因線索(remaining/perfect)
|
||||
if raw_items and not filtered:
|
||||
rems = [r.get("remaining") for r in raw_items if r.get("remaining") is not None]
|
||||
perfects = sum(1 for r in raw_items if r.get("is_perfect"))
|
||||
@@ -290,8 +323,10 @@ def main() -> int:
|
||||
"count_filtered": len(filtered),
|
||||
"items": filtered,
|
||||
"debug": {
|
||||
**(debug if isinstance(debug, dict) else {}),
|
||||
"api_source": "Fresh from Function Server",
|
||||
"notes": merged_notes,
|
||||
"targets": [f"{n}:{la},{ln}" for n, la, ln in targets],
|
||||
"target_stats": target_stats,
|
||||
"api_source": "Fresh from Function Server (multi-target union)",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user