修复今日最高:新增 city_daily_max 表,所有数据源写入日最高
- DB: city_daily_max(icao,max_temp,obs_date,max_time) upsert - AMOS/JMA/KNMI/FMI/HKO/METAR集群/CWA 采集后均写入 - Rust: 读 city_daily_max 获取准确日最高 + 时间 - 卡片恢复 High 行,显示最高温 + 达成时间 - new_high 提醒也基于真实日最高重新生效 Tested: cargo build + ruff check 均通过
This commit is contained in:
@@ -8,6 +8,19 @@ pub struct ObsRow {
|
|||||||
pub created_at: Option<String>,
|
pub created_at: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get today's daily max temperature for an ICAO station.
|
||||||
|
pub fn get_daily_max(db_path: &str, icao: &str) -> Option<(f64, Option<String>)> {
|
||||||
|
let conn = Connection::open(db_path).ok()?;
|
||||||
|
let today = chrono::Utc::now().format("%Y-%m-%d").to_string();
|
||||||
|
let mut stmt = conn
|
||||||
|
.prepare("SELECT max_temp, max_time FROM city_daily_max WHERE icao = ?1 AND obs_date = ?2")
|
||||||
|
.ok()?;
|
||||||
|
stmt.query_row(rusqlite::params![icao.to_uppercase(), today], |row| {
|
||||||
|
Ok((row.get(0)?, row.get(1)?))
|
||||||
|
})
|
||||||
|
.ok()
|
||||||
|
}
|
||||||
|
|
||||||
/// Get recent temperature observations for an ICAO station.
|
/// Get recent temperature observations for an ICAO station.
|
||||||
pub fn get_recent_obs(db_path: &str, icao: &str, minutes: i32, limit: usize) -> Vec<ObsRow> {
|
pub fn get_recent_obs(db_path: &str, icao: &str, minutes: i32, limit: usize) -> Vec<ObsRow> {
|
||||||
let conn = match Connection::open(db_path) {
|
let conn = match Connection::open(db_path) {
|
||||||
|
|||||||
@@ -81,10 +81,10 @@ fn load_city_snapshot(db_path: &str, idx: usize, (key, _zh, en, icao, airport, t
|
|||||||
let trend_data: Vec<f64> = temps.iter().take(6).copied().collect();
|
let trend_data: Vec<f64> = temps.iter().take(6).copied().collect();
|
||||||
let trend = trend::calc_trend(&trend_data);
|
let trend = trend::calc_trend(&trend_data);
|
||||||
|
|
||||||
// Today's max: max of all temps in recent window (approximation)
|
// Today's max: from city_daily_max table (maintained by Python collector)
|
||||||
let today_max = temps.iter().cloned().fold(None::<f64>, |a, b| {
|
let (today_max, max_time) = db::get_daily_max(db_path, icao)
|
||||||
Some(a.map_or(b, |x| x.max(b)))
|
.map(|(t, mt)| (Some(t), mt))
|
||||||
});
|
.unwrap_or((None, None));
|
||||||
let new_high = match (current_temp, today_max) {
|
let new_high = match (current_temp, today_max) {
|
||||||
(Some(ct), Some(tm)) => ct >= tm + 0.3,
|
(Some(ct), Some(tm)) => ct >= tm + 0.3,
|
||||||
_ => false,
|
_ => false,
|
||||||
@@ -111,7 +111,7 @@ fn load_city_snapshot(db_path: &str, idx: usize, (key, _zh, en, icao, airport, t
|
|||||||
local_time,
|
local_time,
|
||||||
current_temp,
|
current_temp,
|
||||||
today_max,
|
today_max,
|
||||||
max_time: None,
|
max_time,
|
||||||
trend,
|
trend,
|
||||||
new_high,
|
new_high,
|
||||||
runway_pairs,
|
runway_pairs,
|
||||||
|
|||||||
@@ -199,6 +199,7 @@ body {
|
|||||||
|
|
||||||
/* ── obs age ── */
|
/* ── obs age ── */
|
||||||
.obs-age { font-size: 13px; color: #5a6170; }
|
.obs-age { font-size: 13px; color: #5a6170; }
|
||||||
|
.max-time { font-size: 12px; color: #4a5160; margin-left: 2px; }
|
||||||
|
|
||||||
/* ── HTMX indicator ── */
|
/* ── HTMX indicator ── */
|
||||||
.htmx-indicator {
|
.htmx-indicator {
|
||||||
|
|||||||
@@ -43,6 +43,21 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-meta">
|
<div class="card-meta">
|
||||||
|
<div class="meta-row">
|
||||||
|
<span class="label">High</span>
|
||||||
|
{% match c.today_max %}
|
||||||
|
{% when Some with (m) %}
|
||||||
|
<span class="value">{{ "{:.1}"|format(m) }}°C</span>
|
||||||
|
{% match c.max_time %}
|
||||||
|
{% when Some with (mt) %}
|
||||||
|
<span class="value max-time">{{ mt }}</span>
|
||||||
|
{% when None %}
|
||||||
|
{% endmatch %}
|
||||||
|
{% when None %}
|
||||||
|
<span class="value na">--</span>
|
||||||
|
{% endmatch %}
|
||||||
|
<span class="trend {{ c.trend.css_class() }}">{{ c.trend.symbol() }}</span>
|
||||||
|
</div>
|
||||||
<div class="meta-row">
|
<div class="meta-row">
|
||||||
<span class="label">Obs</span>
|
<span class="label">Obs</span>
|
||||||
{% match c.obs_age_min %}
|
{% match c.obs_age_min %}
|
||||||
|
|||||||
@@ -377,6 +377,17 @@ class SettlementSourceMixin:
|
|||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("airport_obs_log append failed for cwa")
|
logger.exception("airport_obs_log append failed for cwa")
|
||||||
|
try:
|
||||||
|
from src.database.db_manager import DBManager
|
||||||
|
today = datetime.now().strftime("%Y-%m-%d")
|
||||||
|
tc = payload["current"]["temp"]
|
||||||
|
if tc is not None:
|
||||||
|
DBManager().upsert_daily_max(
|
||||||
|
icao="466920", temp_c=tc, obs_date=today,
|
||||||
|
obs_time=str(obs_time_raw or "").strip(),
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return payload
|
return payload
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning(f"CWA settlement fetch failed: {exc}")
|
logger.warning(f"CWA settlement fetch failed: {exc}")
|
||||||
|
|||||||
@@ -883,6 +883,19 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour
|
|||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("airport_obs_log append failed for metar cluster city={}", city_lower)
|
logger.exception("airport_obs_log append failed for metar cluster city={}", city_lower)
|
||||||
|
# 更新日最高
|
||||||
|
try:
|
||||||
|
today = datetime.now().strftime("%Y-%m-%d")
|
||||||
|
for row in cluster_data:
|
||||||
|
icao = row.get("icao")
|
||||||
|
temp_c = row.get("temp_c") if "temp_c" in row else row.get("temp")
|
||||||
|
if icao and temp_c is not None:
|
||||||
|
DBManager().upsert_daily_max(
|
||||||
|
icao=icao, temp_c=temp_c, obs_date=today,
|
||||||
|
obs_time=str(row.get("obs_time") or ""),
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def _attach_china_official_nearby(
|
def _attach_china_official_nearby(
|
||||||
self, results: Dict, city_lower: str, use_fahrenheit: bool
|
self, results: Dict, city_lower: str, use_fahrenheit: bool
|
||||||
@@ -931,6 +944,15 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour
|
|||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("airport_obs_log append failed for jma city={}", city_lower)
|
logger.exception("airport_obs_log append failed for jma city={}", city_lower)
|
||||||
|
try:
|
||||||
|
today = datetime.now().strftime("%Y-%m-%d")
|
||||||
|
if icao and row.get("temp") is not None:
|
||||||
|
DBManager().upsert_daily_max(
|
||||||
|
icao=icao, temp_c=row.get("temp"), obs_date=today,
|
||||||
|
obs_time=str(row.get("obs_time") or ""),
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def _attach_knmi_official_nearby(
|
def _attach_knmi_official_nearby(
|
||||||
self, results: Dict, city_lower: str, use_fahrenheit: bool
|
self, results: Dict, city_lower: str, use_fahrenheit: bool
|
||||||
@@ -956,6 +978,17 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour
|
|||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("airport_obs_log append failed for knmi city={}", city_lower)
|
logger.exception("airport_obs_log append failed for knmi city={}", city_lower)
|
||||||
|
try:
|
||||||
|
today = datetime.now().strftime("%Y-%m-%d")
|
||||||
|
icao_kn = str(rows[0].get("icao") or "EHAM") if rows else ""
|
||||||
|
temp_kn = rows[0].get("temp") if rows else None
|
||||||
|
if icao_kn and temp_kn is not None:
|
||||||
|
DBManager().upsert_daily_max(
|
||||||
|
icao=icao_kn, temp_c=temp_kn, obs_date=today,
|
||||||
|
obs_time=str(rows[0].get("obs_time") or ""),
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def _attach_fmi_official_nearby(
|
def _attach_fmi_official_nearby(
|
||||||
self, results: Dict, city_lower: str, use_fahrenheit: bool
|
self, results: Dict, city_lower: str, use_fahrenheit: bool
|
||||||
@@ -982,6 +1015,15 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour
|
|||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("airport_obs_log append failed for fmi city={}", city_lower)
|
logger.exception("airport_obs_log append failed for fmi city={}", city_lower)
|
||||||
|
try:
|
||||||
|
today = datetime.now().strftime("%Y-%m-%d")
|
||||||
|
if icao and row.get("temp") is not None:
|
||||||
|
DBManager().upsert_daily_max(
|
||||||
|
icao=icao, temp_c=row.get("temp"), obs_date=today,
|
||||||
|
obs_time=str(row.get("obs_time") or ""),
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def _attach_hko_obs_official_nearby(
|
def _attach_hko_obs_official_nearby(
|
||||||
self, results: Dict, city_lower: str, use_fahrenheit: bool
|
self, results: Dict, city_lower: str, use_fahrenheit: bool
|
||||||
@@ -1007,6 +1049,17 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour
|
|||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("airport_obs_log append failed for hko_obs city={}", city_lower)
|
logger.exception("airport_obs_log append failed for hko_obs city={}", city_lower)
|
||||||
|
try:
|
||||||
|
today = datetime.now().strftime("%Y-%m-%d")
|
||||||
|
icao_hk = str(rows[0].get("icao") or "HKO") if rows else ""
|
||||||
|
temp_hk = rows[0].get("temp") if rows else None
|
||||||
|
if icao_hk and temp_hk is not None:
|
||||||
|
DBManager().upsert_daily_max(
|
||||||
|
icao=icao_hk, temp_c=temp_hk, obs_date=today,
|
||||||
|
obs_time=str(rows[0].get("obs_time") or ""),
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def _attach_korean_amos_data(
|
def _attach_korean_amos_data(
|
||||||
self, results: Dict, city_lower: str, use_fahrenheit: bool
|
self, results: Dict, city_lower: str, use_fahrenheit: bool
|
||||||
@@ -1047,6 +1100,18 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour
|
|||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("airport_obs_log append failed for amos city={}", city_lower)
|
logger.exception("airport_obs_log append failed for amos city={}", city_lower)
|
||||||
|
# 更新日最高
|
||||||
|
try:
|
||||||
|
today = datetime.now().strftime("%Y-%m-%d")
|
||||||
|
icao = amos_data.get("icao") or ""
|
||||||
|
temp_c = amos_data.get("temp_c")
|
||||||
|
if icao and temp_c is not None:
|
||||||
|
DBManager().upsert_daily_max(
|
||||||
|
icao=icao, temp_c=temp_c, obs_date=today,
|
||||||
|
obs_time=amos_data.get("observation_time") or "",
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
else:
|
else:
|
||||||
logger.warning("AMOS: no data returned for city={}", city_lower)
|
logger.warning("AMOS: no data returned for city={}", city_lower)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|||||||
@@ -295,6 +295,15 @@ class DBManager:
|
|||||||
conn.execute(
|
conn.execute(
|
||||||
"CREATE INDEX IF NOT EXISTS idx_airport_obs_log_icao_time ON airport_obs_log(icao, created_at DESC)"
|
"CREATE INDEX IF NOT EXISTS idx_airport_obs_log_icao_time ON airport_obs_log(icao, created_at DESC)"
|
||||||
)
|
)
|
||||||
|
conn.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS city_daily_max (
|
||||||
|
icao TEXT PRIMARY KEY,
|
||||||
|
max_temp REAL NOT NULL,
|
||||||
|
obs_date TEXT NOT NULL,
|
||||||
|
max_time TEXT,
|
||||||
|
updated_at TEXT NOT NULL
|
||||||
|
)
|
||||||
|
""")
|
||||||
self._ensure_column(conn, "users", "daily_points", "INTEGER DEFAULT 0")
|
self._ensure_column(conn, "users", "daily_points", "INTEGER DEFAULT 0")
|
||||||
self._ensure_column(conn, "users", "daily_points_date", "TEXT")
|
self._ensure_column(conn, "users", "daily_points_date", "TEXT")
|
||||||
self._ensure_column(conn, "users", "weekly_points", "INTEGER DEFAULT 0")
|
self._ensure_column(conn, "users", "weekly_points", "INTEGER DEFAULT 0")
|
||||||
@@ -1880,3 +1889,26 @@ class DBManager:
|
|||||||
(str(icao).strip().upper(), str(-int(minutes))),
|
(str(icao).strip().upper(), str(-int(minutes))),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
return [dict(r) for r in rows]
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
def upsert_daily_max(
|
||||||
|
self, *, icao: str, temp_c: float, obs_date: str,
|
||||||
|
max_time: Optional[str] = None, obs_time: str = "",
|
||||||
|
) -> None:
|
||||||
|
"""Only update if temp_c exceeds stored max for the same date."""
|
||||||
|
with self._get_connection() as conn:
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT max_temp, obs_date FROM city_daily_max WHERE icao = ?",
|
||||||
|
(str(icao).strip().upper(),),
|
||||||
|
).fetchone()
|
||||||
|
if row and row[1] == obs_date:
|
||||||
|
if temp_c <= (row[0] or -999.0):
|
||||||
|
return # not a new high
|
||||||
|
# new day or new high: upsert
|
||||||
|
conn.execute(
|
||||||
|
"""INSERT OR REPLACE INTO city_daily_max
|
||||||
|
(icao, max_temp, obs_date, max_time, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?)""",
|
||||||
|
(str(icao).strip().upper(), temp_c, obs_date,
|
||||||
|
max_time or "", obs_time or datetime.now().isoformat()),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|||||||
Reference in New Issue
Block a user