37494a7192
将 web/routes.py 拆分为模块化 router + service 架构
- 新增 web/app_factory.py 集中注册 7 个域名 router
- 新增 web/routers/ 薄壳路由层(auth/city/system/scan/ops/payments/analytics)
- 新增 web/services/ 业务函数下沉(每域独立 service 文件)
- web/routes.py 缩减为 city_runtime 的兼容重导出 facade
- analysis_service.py/app.py 适配新入口并清理冗余导入
Scope-risk: LOW — 全量 170 测试通过,router 注册顺序与原路由一致
Tested: python -m pytest -q (170 passed), ruff check . (All checks passed)
@
229 lines
9.8 KiB
Python
229 lines
9.8 KiB
Python
"""City API service functions used by the city router."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Dict, Optional
|
|
|
|
from fastapi import BackgroundTasks, HTTPException, Request
|
|
from fastapi.concurrency import run_in_threadpool
|
|
from loguru import logger
|
|
|
|
import web.routes as legacy_routes
|
|
|
|
|
|
def _build_cities_payload() -> Dict[str, Any]:
|
|
out = []
|
|
deb_recent_index = legacy_routes._build_recent_deb_performance_index()
|
|
for name, info in legacy_routes.CITIES.items():
|
|
risk = legacy_routes.CITY_RISK_PROFILES.get(name, {})
|
|
city_meta = legacy_routes.CITY_REGISTRY.get(name, {}) or {}
|
|
deb_recent = deb_recent_index.get(name, {})
|
|
settlement_source = str(info.get("settlement_source") or "metar").strip().lower() or "metar"
|
|
provider = legacy_routes.get_country_network_provider(name)
|
|
out.append(
|
|
{
|
|
"name": name,
|
|
"display_name": str(city_meta.get("display_name") or city_meta.get("name") or name.title()),
|
|
"lat": info["lat"],
|
|
"lon": info["lon"],
|
|
"utc_offset_seconds": legacy_routes.get_city_utc_offset_seconds(name),
|
|
"risk_level": risk.get("risk_level", "low"),
|
|
"risk_emoji": risk.get("risk_emoji", "🟢"),
|
|
"airport": risk.get("airport_name", ""),
|
|
"icao": risk.get("icao", ""),
|
|
"temp_unit": "fahrenheit" if info["f"] else "celsius",
|
|
"is_major": city_meta.get("is_major", True),
|
|
"settlement_source": settlement_source,
|
|
"settlement_source_label": legacy_routes.SETTLEMENT_SOURCE_LABELS.get(
|
|
settlement_source,
|
|
settlement_source.upper(),
|
|
),
|
|
"settlement_station_code": city_meta.get("settlement_station_code") or city_meta.get("icao"),
|
|
"settlement_station_label": city_meta.get("settlement_station_label") or city_meta.get("airport_name"),
|
|
"network_provider": provider.provider_code,
|
|
"network_provider_label": provider.provider_label,
|
|
"deb_recent_tier": deb_recent.get("tier", "other"),
|
|
"deb_recent_hit_rate": deb_recent.get("hit_rate"),
|
|
"deb_recent_sample_count": deb_recent.get("sample_count", 0),
|
|
"deb_recent_mae": deb_recent.get("mae"),
|
|
"deb_recent_last_date": deb_recent.get("last_date"),
|
|
}
|
|
)
|
|
return {"cities": out}
|
|
|
|
|
|
async def list_cities_payload(_request: Request) -> Dict[str, Any]:
|
|
try:
|
|
return await run_in_threadpool(_build_cities_payload)
|
|
except Exception as exc:
|
|
logger.error(f"Error in list_cities: {exc}")
|
|
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
|
|
|
|
|
async def get_city_detail_payload(
|
|
request: Request,
|
|
name: str,
|
|
*,
|
|
force_refresh: bool = False,
|
|
depth: str = "panel",
|
|
) -> Dict[str, Any]:
|
|
legacy_routes._assert_entitlement(request)
|
|
city = legacy_routes._normalize_city_or_404(name)
|
|
normalized_depth = str(depth or "panel").strip().lower()
|
|
if normalized_depth == "full":
|
|
detail_mode = "full"
|
|
elif normalized_depth == "market":
|
|
detail_mode = "market"
|
|
elif normalized_depth == "nearby":
|
|
detail_mode = "nearby"
|
|
else:
|
|
detail_mode = "panel"
|
|
if detail_mode == "panel":
|
|
if force_refresh:
|
|
return await run_in_threadpool(legacy_routes._refresh_city_panel_cache, city, True)
|
|
cached_entry = await run_in_threadpool(legacy_routes._CACHE_DB.get_city_cache, "panel", city)
|
|
if cached_entry:
|
|
if not legacy_routes._city_cache_is_fresh(cached_entry, legacy_routes.CITY_PANEL_CACHE_TTL_SEC):
|
|
return await run_in_threadpool(legacy_routes._refresh_city_panel_cache, city, False)
|
|
return cached_entry.get("payload") or {}
|
|
return await run_in_threadpool(legacy_routes._refresh_city_panel_cache, city, False)
|
|
if detail_mode == "nearby":
|
|
if force_refresh:
|
|
return await run_in_threadpool(legacy_routes._refresh_city_nearby_cache, city, True)
|
|
cached_entry = await run_in_threadpool(legacy_routes._CACHE_DB.get_city_cache, "nearby", city)
|
|
if cached_entry:
|
|
if not legacy_routes._city_cache_is_fresh(cached_entry, legacy_routes.CITY_NEARBY_CACHE_TTL_SEC):
|
|
return await run_in_threadpool(legacy_routes._refresh_city_nearby_cache, city, False)
|
|
return cached_entry.get("payload") or {}
|
|
return await run_in_threadpool(legacy_routes._refresh_city_nearby_cache, city, False)
|
|
if detail_mode == "market":
|
|
if force_refresh:
|
|
return await run_in_threadpool(legacy_routes._refresh_city_market_cache, city, True)
|
|
cached_entry = await run_in_threadpool(legacy_routes._CACHE_DB.get_city_cache, "market", city)
|
|
if cached_entry:
|
|
if not legacy_routes._market_analysis_cache_is_fresh(cached_entry):
|
|
return await run_in_threadpool(legacy_routes._refresh_city_market_cache, city, False)
|
|
return cached_entry.get("payload") or {}
|
|
return await run_in_threadpool(legacy_routes._refresh_city_market_cache, city, False)
|
|
return await run_in_threadpool(legacy_routes._analyze, city, force_refresh, False, detail_mode)
|
|
|
|
|
|
async def get_city_history_payload(
|
|
request: Request,
|
|
name: str,
|
|
*,
|
|
include_records: bool = False,
|
|
) -> Dict[str, Any]:
|
|
legacy_routes._assert_entitlement(request)
|
|
city = legacy_routes._normalize_city_or_404(name)
|
|
if include_records:
|
|
return await run_in_threadpool(legacy_routes._build_city_history_payload, city, True)
|
|
cached_entry = await run_in_threadpool(legacy_routes._CACHE_DB.get_city_cache, "history_preview", city)
|
|
if cached_entry:
|
|
if not legacy_routes._city_cache_is_fresh(cached_entry, legacy_routes.CITY_HISTORY_PREVIEW_CACHE_TTL_SEC):
|
|
return await run_in_threadpool(legacy_routes._refresh_city_history_preview_cache, city)
|
|
return cached_entry.get("payload") or {}
|
|
return await run_in_threadpool(legacy_routes._refresh_city_history_preview_cache, city)
|
|
|
|
|
|
async def get_city_summary_payload(
|
|
_request: Request,
|
|
name: str,
|
|
*,
|
|
force_refresh: bool = False,
|
|
) -> Dict[str, Any]:
|
|
city = legacy_routes._normalize_city_or_404(name)
|
|
if force_refresh:
|
|
return await run_in_threadpool(legacy_routes._refresh_city_summary_cache, city, True)
|
|
cached_entry = await run_in_threadpool(legacy_routes._CACHE_DB.get_city_cache, "summary", city)
|
|
if cached_entry:
|
|
if not legacy_routes._city_cache_is_fresh(cached_entry, legacy_routes.CITY_SUMMARY_CACHE_TTL_SEC):
|
|
return await run_in_threadpool(legacy_routes._refresh_city_summary_cache, city, False)
|
|
return cached_entry.get("payload") or {}
|
|
return await run_in_threadpool(legacy_routes._refresh_city_summary_cache, city, False)
|
|
|
|
|
|
async def get_city_detail_aggregate_payload(
|
|
request: Request,
|
|
name: str,
|
|
*,
|
|
force_refresh: bool = False,
|
|
market_slug: Optional[str] = None,
|
|
target_date: Optional[str] = None,
|
|
) -> Dict[str, Any]:
|
|
legacy_routes._assert_entitlement(request)
|
|
city = legacy_routes._normalize_city_or_404(name)
|
|
data = await run_in_threadpool(legacy_routes._analyze, city, force_refresh, True)
|
|
return await run_in_threadpool(
|
|
legacy_routes._build_city_detail_payload,
|
|
data,
|
|
market_slug,
|
|
target_date,
|
|
)
|
|
|
|
|
|
async def get_city_market_scan_payload(
|
|
request: Request,
|
|
background_tasks: BackgroundTasks,
|
|
name: str,
|
|
*,
|
|
force_refresh: bool = False,
|
|
market_slug: Optional[str] = None,
|
|
target_date: Optional[str] = None,
|
|
lite: bool = False,
|
|
) -> Dict[str, Any]:
|
|
legacy_routes._assert_entitlement(request)
|
|
city = legacy_routes._normalize_city_or_404(name)
|
|
if force_refresh:
|
|
data = await run_in_threadpool(legacy_routes._refresh_city_market_cache, city, True)
|
|
cached_scan = legacy_routes._get_cached_market_scan_payload(
|
|
data,
|
|
market_slug=market_slug,
|
|
target_date=target_date,
|
|
lite=lite,
|
|
)
|
|
if cached_scan is not None:
|
|
return cached_scan
|
|
else:
|
|
cached_entry = await run_in_threadpool(legacy_routes._CACHE_DB.get_city_cache, "market", city)
|
|
if cached_entry:
|
|
data = cached_entry.get("payload") or {}
|
|
cached_scan = legacy_routes._get_cached_market_scan_payload(
|
|
data,
|
|
market_slug=market_slug,
|
|
target_date=target_date,
|
|
lite=lite,
|
|
)
|
|
if cached_scan is not None:
|
|
if not legacy_routes._market_analysis_cache_is_fresh(cached_entry):
|
|
legacy_routes._schedule_cache_refresh(background_tasks, kind="market", city=city)
|
|
return cached_scan
|
|
if legacy_routes._market_analysis_cache_is_fresh(cached_entry):
|
|
return await run_in_threadpool(
|
|
legacy_routes._refresh_market_scan_payload_from_cached_analysis,
|
|
city,
|
|
data,
|
|
market_slug=market_slug,
|
|
target_date=target_date,
|
|
lite=lite,
|
|
)
|
|
else:
|
|
legacy_routes._schedule_cache_refresh(background_tasks, kind="market", city=city)
|
|
else:
|
|
data = await run_in_threadpool(legacy_routes._refresh_city_market_cache, city, False)
|
|
cached_scan = legacy_routes._get_cached_market_scan_payload(
|
|
data,
|
|
market_slug=market_slug,
|
|
target_date=target_date,
|
|
lite=lite,
|
|
)
|
|
if cached_scan is not None:
|
|
return cached_scan
|
|
return await run_in_threadpool(
|
|
legacy_routes._build_city_market_scan_payload,
|
|
data,
|
|
market_slug,
|
|
target_date,
|
|
lite,
|
|
)
|