Add Qingdao to the tradable city network

Qingdao needs the same airport-settlement path as the other Wunderground-backed APAC cities, so the registry, aliases, timezone, prewarm, official links, market focus, and tests now point to ZSQD / Qingdao Jiaodong International Airport.

Constraint: User supplied Wunderground Qingdao/ZSQD settlement URL.
Rejected: Add a partial registry-only entry | it would show in APIs without frontend links, prewarm coverage, or alias support.
Confidence: high
Scope-risk: narrow
Reversibility: clean
Tested: pytest tests/test_country_networks.py tests/test_web_observability.py::test_cities_endpoint_includes_new_wunderground_cities -q
Tested: npm run build
Not-tested: Live Wunderground fetch for ZSQD in production.
This commit is contained in:
2569718930@qq.com
2026-04-28 07:08:33 +08:00
parent e6010d8242
commit 89a71d1bc0
14 changed files with 62 additions and 2 deletions
+1
View File
@@ -2,6 +2,7 @@
## 1.5.5 - 2026-04-27
- 新增 Qingdao / 青岛城市,结算锚点接入 Wunderground 青岛胶东国际机场 `ZSQD` 历史页,并补齐别名、时区、预热、官方来源和前端地区归类
- 城市决策卡新增 AI 机场报文解读缓存说明:页面内存缓存保留 loading / 流式片段 / 最终结果,`localStorage` 保存最终成功 payload,后端 AI 缓存不再因 `local_time` 变化失效
- 城市决策卡兜底文案明确标记“快速证据模式”,避免在 DeepSeek 未完整返回时误写成“AI 机场报文解读正常”
- 城市决策卡流式 AI 解读改为只请求 METAR/官方观测核心解读与判断依据,最高温中枢、模型一致性和风险清单由后端规则补齐,减少等待时间
+1 -1
View File
@@ -97,7 +97,7 @@ flowchart LR
## Monitored Cities (52)
- Europe / Middle East / Africa: Ankara, Istanbul, Moscow, London, Paris, Munich, Milan, Warsaw, Madrid, Tel Aviv, Amsterdam, Helsinki, Lagos, Cape Town, Jeddah
- APAC: Seoul, Busan, Hong Kong, Lau Fau Shan, Taipei, Shanghai, Beijing, Wuhan, Chengdu, Chongqing, Shenzhen, Guangzhou, Singapore, Tokyo, Kuala Lumpur, Jakarta, Manila, Wellington
- APAC: Seoul, Busan, Hong Kong, Lau Fau Shan, Taipei, Shanghai, Beijing, Qingdao, Wuhan, Chengdu, Chongqing, Shenzhen, Guangzhou, Singapore, Tokyo, Kuala Lumpur, Jakarta, Manila, Wellington
- Americas: Toronto, New York, Los Angeles, San Francisco, Aurora, Austin, Houston, Chicago, Dallas, Miami, Atlanta, Seattle, Mexico City, Buenos Aires, Sao Paulo, Panama City
- South Asia: Lucknow, Karachi, Masroor Air Base
+2
View File
@@ -41,6 +41,7 @@ const CITY_NAME_ZH: Record<string, string> = {
Panama: "巴拿马城",
"Panama City": "巴拿马城",
Paris: "巴黎",
Qingdao: "青岛",
"Sao Paulo": "圣保罗",
"São Paulo": "圣保罗",
Seattle: "西雅图",
@@ -91,6 +92,7 @@ const AIRPORT_NAME_ZH: Record<string, string> = {
"New York": "拉瓜迪亚机场",
"Panama City": "托库门国际机场",
Paris: "戴高乐机场",
Qingdao: "青岛胶东国际机场",
"Sao Paulo": "瓜鲁柳斯国际机场",
"São Paulo": "瓜鲁柳斯国际机场",
Seattle: "西雅图-塔科马国际机场",
@@ -617,6 +617,23 @@ const CITY_SPECIFIC_SOURCES: Record<string, OfficialSourceLink[]> = {
kind: "metar",
},
],
qingdao: [
{
label: "Wunderground ZSQD",
href: "https://www.wunderground.com/history/daily/cn/qingdao/ZSQD",
kind: "agency",
},
{
label: "青岛胶东国际机场",
href: "https://www.qdairport.com/",
kind: "airport",
},
{
label: "ZSQD METAR",
href: "https://aviationweather.gov/data/metar/?id=ZSQD&decoded=1&taf=1",
kind: "metar",
},
],
"kuala lumpur": [
{
label: "Wunderground WMKK",
+1
View File
@@ -25,6 +25,7 @@ const CITY_TO_MARKET_SLUG: Record<string, string> = {
munich: "munich",
"new york": "nyc",
paris: "paris",
qingdao: "qingdao",
"san francisco": "san-francisco",
"sao paulo": "sao-paulo",
seattle: "seattle",
+1
View File
@@ -107,6 +107,7 @@ const CITY_REGION_FALLBACK: Record<string, MarketRegionKey> = {
chongqing: "asia_pacific",
shenzhen: "asia_pacific",
guangzhou: "asia_pacific",
qingdao: "asia_pacific",
beijing: "asia_pacific",
wuhan: "asia_pacific",
};
+23
View File
@@ -715,6 +715,24 @@ CITY_REGISTRY = {
"distance_km": 28.0,
"warning": "市场现按 Wunderground 广州白云国际机场站整度°C口径结算;珠三角海陆风与午后对流会让峰值兑现时间出现摇摆。",
},
"qingdao": {
"name": "Qingdao",
"lat": 36.3620,
"lon": 120.0882,
"icao": "ZSQD",
"settlement_source": "wunderground",
"settlement_station_code": "ZSQD",
"settlement_station_label": "Qingdao Jiaodong International Airport Station",
"settlement_url": "https://www.wunderground.com/history/daily/cn/qingdao/ZSQD",
"tz_offset": 28800,
"use_fahrenheit": False,
"is_major": True,
"risk_level": "medium",
"risk_emoji": "🟡",
"airport_name": "青岛胶东国际机场",
"distance_km": 39.0,
"warning": "市场现按 Wunderground 青岛胶东国际机场站整度°C口径结算;沿海海风、低云和海雾会影响日内升温效率。",
},
"beijing": {
"name": "Beijing",
"lat": 40.0801,
@@ -918,12 +936,17 @@ ALIASES = {
"ckg": "chongqing", "cq": "chongqing",
"szx": "shenzhen", "sz": "shenzhen",
"can": "guangzhou", "gz": "guangzhou", "zggg": "guangzhou",
"tao": "qingdao", "qdo": "qingdao", "qingdao": "qingdao", "zsqd": "qingdao",
"pek": "beijing", "bjs": "beijing", "bj": "beijing",
"wuh": "wuhan", "wh": "wuhan",
"成都": "chengdu",
"重庆": "chongqing",
"深圳": "shenzhen",
"广州": "guangzhou",
"青岛": "qingdao",
"青島": "qingdao",
"青岛胶东": "qingdao",
"青島膠東": "qingdao",
"北京": "beijing",
"武汉": "wuhan",
}
+1
View File
@@ -50,6 +50,7 @@ CITY_TIME_ZONES = {
"new york": "America/New_York",
"panama city": "America/Panama",
"paris": "Europe/Paris",
"qingdao": "Asia/Shanghai",
"san francisco": "America/Los_Angeles",
"sao paulo": "America/Sao_Paulo",
"seattle": "America/Los_Angeles",
+1
View File
@@ -13,6 +13,7 @@ CHINA_CMA_CITIES = {
"chengdu",
"chongqing",
"guangzhou",
"qingdao",
"shanghai",
"shenzhen",
"wuhan",
+4
View File
@@ -54,6 +54,7 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour
"chengdu": ["ZUUU", "ZUTF"],
"chongqing": ["ZUCK", "ZUPS"],
"guangzhou": ["ZGGG", "ZGSZ"],
"qingdao": ["ZSQD"],
"shenzhen": ["ZGSZ", "ZGGG"],
"beijing": ["ZBAA", "ZBAD"],
"wuhan": ["ZHHH", "ZHES"],
@@ -600,6 +601,9 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour
"重庆": "Chongqing",
"shenzhen": "Shenzhen",
"深圳": "Shenzhen",
"qingdao": "Qingdao",
"青岛": "Qingdao",
"青島": "Qingdao",
"beijing": "Beijing",
"北京": "Beijing",
"wuhan": "Wuhan",
+1
View File
@@ -21,6 +21,7 @@ DEFAULT_CITIES = [
"beijing",
"shenzhen",
"guangzhou",
"qingdao",
"wuhan",
"chengdu",
"chongqing",
+6 -1
View File
@@ -17,17 +17,21 @@ class _DummyMetarSource(MetarSourceMixin):
metar_fast_cache_ttl_sec = 60
def test_new_south_asia_city_registry_entries_are_wired():
def test_new_city_registry_entries_are_wired():
assert CITY_REGISTRY["manila"]["settlement_source"] == "wunderground"
assert CITY_REGISTRY["manila"]["settlement_station_code"] == "RPLL"
assert CITY_REGISTRY["karachi"]["settlement_source"] == "wunderground"
assert CITY_REGISTRY["karachi"]["settlement_station_code"] == "OPKC"
assert CITY_REGISTRY["qingdao"]["settlement_source"] == "wunderground"
assert CITY_REGISTRY["qingdao"]["settlement_station_code"] == "ZSQD"
assert CITY_REGISTRY["masroor air base"]["icao"] == "OPMR"
assert ALIASES["rpll"] == "manila"
assert ALIASES["opkc"] == "karachi"
assert ALIASES["zsqd"] == "qingdao"
assert ALIASES["opmr"] == "masroor air base"
assert CITIES["manila"]["lat"] == CITY_REGISTRY["manila"]["lat"]
assert CITIES["karachi"]["lon"] == CITY_REGISTRY["karachi"]["lon"]
assert CITIES["qingdao"]["lat"] == CITY_REGISTRY["qingdao"]["lat"]
assert CITIES["masroor air base"]["settlement_source"] == "metar"
@@ -38,6 +42,7 @@ def test_city_time_zone_mapping_covers_every_city_and_dst_offsets():
april = datetime(2026, 4, 19, 6, 30, tzinfo=timezone.utc)
assert get_city_utc_offset_seconds("guangzhou", april) == 28800
assert get_city_utc_offset_seconds("qingdao", april) == 28800
assert get_city_utc_offset_seconds("ankara", april) == 10800
assert get_city_utc_offset_seconds("london", april) == 3600
assert get_city_utc_offset_seconds("paris", april) == 7200
+1
View File
@@ -590,6 +590,7 @@ def test_cities_endpoint_includes_new_wunderground_cities():
names = {item["name"] for item in payload["cities"]}
assert {
"busan",
"qingdao",
"panama city",
"kuala lumpur",
"jakarta",
+2
View File
@@ -93,6 +93,7 @@ DEFAULT_PREWARM_CITIES = [
"beijing",
"shenzhen",
"guangzhou",
"qingdao",
"wuhan",
"chengdu",
"chongqing",
@@ -116,6 +117,7 @@ ASIA_CORE_CITIES = [
"shanghai",
"beijing",
"guangzhou",
"qingdao",
"shenzhen",
"chongqing",
"chengdu",