From 9ed31b95a4d0c97335184feb363b53e3b9d0a479 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Thu, 16 Apr 2026 18:21:51 +0800 Subject: [PATCH] Fix METAR refresh timing and local observation display --- frontend/lib/dashboard-utils.ts | 20 ++++++++++++++------ src/data_collection/city_registry.py | 2 ++ src/data_collection/metar_sources.py | 25 ++++++++++++++++++++++++- src/data_collection/weather_sources.py | 3 +++ tests/test_country_networks.py | 16 ++++++++++++++++ web/analysis_service.py | 13 +++++++++++-- 6 files changed, 70 insertions(+), 9 deletions(-) diff --git a/frontend/lib/dashboard-utils.ts b/frontend/lib/dashboard-utils.ts index 686cfda9..a0419292 100644 --- a/frontend/lib/dashboard-utils.ts +++ b/frontend/lib/dashboard-utils.ts @@ -2993,6 +2993,14 @@ function formatObservationUpdate(value: unknown, locale: Locale) { return normalizeHm(raw) || raw; } +function localObservationTimeCandidate(value: unknown) { + const raw = String(value ?? "").trim(); + if (!raw || raw.includes("T") || /^\d{4}-\d{2}-\d{2}/.test(raw)) { + return ""; + } + return normalizeHm(raw) || raw; +} + function getOfficialObservationCandidates(detail: CityDetail) { const officialNearby = Array.isArray(detail.official_nearby) ? detail.official_nearby @@ -3078,15 +3086,15 @@ function getObservationUpdateProfile(detail: CityDetail, locale: Locale) { .toLowerCase(); const isNmcCurrent = currentSource === "nmc" || currentSource.includes("nmc"); const rawValue = firstNonEmptyString([ - detail.airport_primary?.obs_time, - detail.airport_primary?.report_time, - detail.airport_current?.obs_time, - detail.airport_current?.report_time, isNmcCurrent ? "" : detail.current?.obs_time, + localObservationTimeCandidate(detail.airport_primary?.obs_time), + localObservationTimeCandidate(detail.airport_current?.obs_time), + localObservationTimeCandidate(mgmFirstRecord?.obs_time), + localObservationTimeCandidate(mgmFirstRecord?.time), + detail.airport_primary?.report_time, + detail.airport_current?.report_time, isNmcCurrent ? "" : detail.current?.report_time, - mgmFirstRecord?.obs_time, mgmFirstRecord?.report_time, - mgmFirstRecord?.time, detail.updated_at, ]); diff --git a/src/data_collection/city_registry.py b/src/data_collection/city_registry.py index 121d27d2..9f5ad70b 100644 --- a/src/data_collection/city_registry.py +++ b/src/data_collection/city_registry.py @@ -10,6 +10,7 @@ CITY_REGISTRY = { "tz_offset": 10800, "use_fahrenheit": False, "is_major": False, + "fast_metar_refresh": True, "risk_level": "medium", "risk_emoji": "🟡", "airport_name": "Esenboğa 机场", @@ -27,6 +28,7 @@ CITY_REGISTRY = { "tz_offset": 10800, "use_fahrenheit": False, "is_major": True, + "fast_metar_refresh": True, "risk_level": "medium", "risk_emoji": "🟡", "airport_name": "Istanbul Airport", diff --git a/src/data_collection/metar_sources.py b/src/data_collection/metar_sources.py index b0a4aefd..889d783e 100644 --- a/src/data_collection/metar_sources.py +++ b/src/data_collection/metar_sources.py @@ -51,6 +51,28 @@ class MetarSourceMixin: return False return True + def _metar_cache_ttl_for_city(self, city: str, icao: Optional[str] = None) -> int: + normalized = str(city or "").strip().lower() + city_meta = (getattr(self, "CITY_REGISTRY", {}) or {}).get(normalized) or {} + if not city_meta and icao: + icao_upper = str(icao or "").strip().upper() + for candidate in (getattr(self, "CITY_REGISTRY", {}) or {}).values(): + if str(candidate.get("icao") or "").strip().upper() == icao_upper: + city_meta = candidate + break + + raw_ttl = city_meta.get("metar_cache_ttl_sec") + try: + ttl = int(raw_ttl) + except (TypeError, ValueError): + ttl = 0 + if ttl > 0: + return ttl + + if city_meta.get("fast_metar_refresh"): + return max(1, int(getattr(self, "metar_fast_cache_ttl_sec", 60))) + return max(1, int(getattr(self, "metar_cache_ttl_sec", 600))) + def get_icao_code(self, city: str) -> Optional[str]: """根据城市名获取对应的 ICAO 机场代码""" normalized = city.lower().strip() @@ -74,9 +96,10 @@ class MetarSourceMixin: cache_key = f"{icao}:{utc_offset}:{use_fahrenheit}" now_ts = time.time() + cache_ttl_sec = self._metar_cache_ttl_for_city(city, icao) with self._metar_cache_lock: cached = self._metar_cache.get(cache_key) - if cached and now_ts - cached["t"] < self.metar_cache_ttl_sec: + if cached and now_ts - cached["t"] < cache_ttl_sec: logger.debug(f"METAR cache hit {icao} age={int(now_ts - cached['t'])}s") record_source_call("metar", "current", "cache_hit", (time.perf_counter() - started) * 1000.0) return cached["d"] diff --git a/src/data_collection/weather_sources.py b/src/data_collection/weather_sources.py index f9e82371..e9fdd5f4 100644 --- a/src/data_collection/weather_sources.py +++ b/src/data_collection/weather_sources.py @@ -184,6 +184,9 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour self.metar_cache_ttl_sec = int( os.getenv("METAR_CACHE_TTL_SEC", "600") # 默认 10 分钟 ) + self.metar_fast_cache_ttl_sec = int( + os.getenv("METAR_FAST_CACHE_TTL_SEC", "60") + ) self._metar_cache: Dict[str, Dict] = {} self._metar_cache_lock = threading.Lock() self.taf_cache_ttl_sec = int( diff --git a/tests/test_country_networks.py b/tests/test_country_networks.py index 66f086c5..33134f56 100644 --- a/tests/test_country_networks.py +++ b/tests/test_country_networks.py @@ -1,9 +1,17 @@ from src.data_collection.country_networks import build_country_network_snapshot from src.data_collection.city_registry import ALIASES, CITY_REGISTRY +from src.data_collection.metar_sources import MetarSourceMixin from web.analysis_service import _build_city_detail_payload, _build_intraday_meteorology from web.core import CITIES +class _DummyMetarSource(MetarSourceMixin): + CITY_REGISTRY = CITY_REGISTRY + CITY_TO_ICAO = {key: value["icao"] for key, value in CITY_REGISTRY.items() if value.get("icao")} + metar_cache_ttl_sec = 600 + metar_fast_cache_ttl_sec = 60 + + def test_new_south_asia_city_registry_entries_are_wired(): assert CITY_REGISTRY["manila"]["settlement_source"] == "wunderground" assert CITY_REGISTRY["manila"]["settlement_station_code"] == "RPLL" @@ -18,6 +26,14 @@ def test_new_south_asia_city_registry_entries_are_wired(): assert CITIES["masroor air base"]["settlement_source"] == "metar" +def test_turkey_metar_uses_fast_cache_ttl(): + source = _DummyMetarSource() + + assert source._metar_cache_ttl_for_city("ankara", "LTAC") == 60 + assert source._metar_cache_ttl_for_city("istanbul", "LTFM") == 60 + assert source._metar_cache_ttl_for_city("karachi", "OPKC") == 600 + + def test_turkey_mgm_provider_returns_official_nearby_rows(): raw = { "metar": { diff --git a/web/analysis_service.py b/web/analysis_service.py index f8426709..84008bce 100644 --- a/web/analysis_service.py +++ b/web/analysis_service.py @@ -1650,6 +1650,15 @@ def _analyze( int(utc_offset or 0), ) + airport_primary_current = dict(network_snapshot.get("airport_primary_current") or {}) + if ( + airport_primary_current.get("source_code") == "metar" + and obs_time_str + and not use_settlement_current + ): + airport_primary_current["obs_time"] = obs_time_str + airport_primary_current["obs_age_min"] = metar_age_min + settlement_today_obs = [] if use_settlement_current: explicit_settlement_obs = settlement_current.get("today_obs") or [] @@ -2266,7 +2275,7 @@ def _analyze( }, "airport_current": { "temp": _sf(mc.get("temp")), - "obs_time": metar.get("obs_time"), + "obs_time": obs_time_str, "max_so_far": airport_max_so_far, "max_temp_time": airport_max_temp_time, "obs_age_min": metar_age_min, @@ -2283,7 +2292,7 @@ def _analyze( "source_label": "METAR", }, "settlement_station": network_snapshot.get("settlement_station") or {}, - "airport_primary": network_snapshot.get("airport_primary_current") or {}, + "airport_primary": airport_primary_current, "airport_primary_today_obs": network_snapshot.get("airport_primary_today_obs") or [], "official_nearby": network_snapshot.get("official_nearby") or [], "official_network_source": network_snapshot.get("official_network_source"),