feat: 新增彩云气象数据下载的时间控制

This commit is contained in:
yarnom 2025-09-29 12:36:06 +08:00
parent aa53a21685
commit 0b0512f5b2
3 changed files with 97 additions and 18 deletions

View File

@ -50,6 +50,7 @@ func StartGinServer() error {
api.GET("/radar/weather_at", radarWeatherAtHandler)
api.GET("/radar/weather_aliases", radarWeatherAliasesHandler)
api.GET("/radar/aliases", radarConfigAliasesHandler)
api.GET("/radar/weather_nearest", radarWeatherNearestHandler)
}
// 获取配置的Web端口

View File

@ -520,6 +520,86 @@ func radarWeatherAtHandler(c *gin.Context) {
c.JSON(http.StatusOK, resp)
}
// radarWeatherNearestHandler returns the nearest radar_weather record to the given dt.
// prefer=lte will pick the latest record not later than dt; else chooses absolute nearest.
func radarWeatherNearestHandler(c *gin.Context) {
alias := c.Query("alias")
if alias == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少 alias 参数"})
return
}
dtStr := c.Query("dt")
if dtStr == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少 dt 参数YYYY-MM-DD HH:MM:SS"})
return
}
prefer := c.DefaultQuery("prefer", "lte") // lte|nearest
loc, _ := time.LoadLocation("Asia/Shanghai")
if loc == nil {
loc = time.FixedZone("CST", 8*3600)
}
target, err := time.ParseInLocation("2006-01-02 15:04:05", dtStr, loc)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 dt 格式"})
return
}
var row *sql.Row
db := database.GetDB()
if prefer == "nearest" {
const q = `
SELECT alias, lat, lon, dt,
temperature, humidity, cloudrate, visibility, dswrf,
wind_speed, wind_direction, pressure
FROM radar_weather
WHERE alias = $1
ORDER BY ABS(EXTRACT(EPOCH FROM (dt - $2))) ASC
LIMIT 1`
row = db.QueryRow(q, alias, target)
} else { // lte default
const q = `
SELECT alias, lat, lon, dt,
temperature, humidity, cloudrate, visibility, dswrf,
wind_speed, wind_direction, pressure
FROM radar_weather
WHERE alias = $1 AND dt <= $2
ORDER BY dt DESC
LIMIT 1`
row = db.QueryRow(q, alias, target)
}
var r radarWeatherRecord
if err := row.Scan(
&r.Alias, &r.Lat, &r.Lon, &r.DT,
&r.Temperature, &r.Humidity, &r.Cloudrate, &r.Visibility, &r.Dswrf,
&r.WindSpeed, &r.WindDirection, &r.Pressure,
); err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "未找到就近的实时气象数据"})
return
}
ageMin := int(target.Sub(r.DT).Minutes())
if ageMin < 0 { // for nearest mode, could be future relative to target
ageMin = -ageMin
}
resp := gin.H{
"alias": r.Alias,
"lat": r.Lat,
"lon": r.Lon,
"dt": r.DT.Format("2006-01-02 15:04:05"),
"temperature": r.Temperature,
"humidity": r.Humidity,
"cloudrate": r.Cloudrate,
"visibility": r.Visibility,
"dswrf": r.Dswrf,
"wind_speed": r.WindSpeed,
"wind_direction": r.WindDirection,
"pressure": r.Pressure,
"age_minutes": ageMin,
"stale": ageMin > 120,
}
c.JSON(http.StatusOK, resp)
}
// radarWeatherAliasesHandler 返回 radar_weather 中存在的站点别名及经纬度(按最近记录去重)
func radarWeatherAliasesHandler(c *gin.Context) {
const q = `

View File

@ -113,11 +113,16 @@
setRealtimeBox(j);
}
async function loadRealtimeAt(alias, dtStr) {
const res = await fetch(`/api/radar/weather_at?alias=${encodeURIComponent(alias)}&dt=${encodeURIComponent(dtStr)}`);
if (!res.ok) { return false; }
async function loadRealtimeNearest(alias, dtStr) {
const res = await fetch(`/api/radar/weather_nearest?prefer=lte&alias=${encodeURIComponent(alias)}&dt=${encodeURIComponent(dtStr)}`);
if (!res.ok) return false;
const j = await res.json();
setRealtimeBox(j);
const bar = document.getElementById('rt_stale');
if (bar) {
if (j.stale) { bar.classList.remove('hidden'); }
else { bar.classList.add('hidden'); }
}
return true;
}
@ -152,13 +157,8 @@
const sel = document.getElementById('timeSelect');
sel.value = t.dt;
}
// 同步气象按瓦片时间向下取整到10分钟查询该桶的实况
if (gAlias) {
const bucket = dtToBucket10(t.dt);
if (bucket) {
await loadRealtimeAt(gAlias, bucket);
}
}
// 同步气象:就近(优先<=)匹配该瓦片时间
if (gAlias) { try { await loadRealtimeNearest(gAlias, t.dt); } catch(e){} }
maybeCalcSector();
maybePlotSquare();
maybeCalcNearbyRain();
@ -188,10 +188,7 @@
document.getElementById('tile_res').textContent = fmt(t.res_deg, 6);
status.textContent = '';
renderTilePlot(t);
if (gAlias) {
const bucket = dtToBucket10(t.dt);
if (bucket) { await loadRealtimeAt(gAlias, bucket); }
}
if (gAlias) { try { await loadRealtimeNearest(gAlias, t.dt); } catch(e){} }
maybeCalcSector();
maybePlotSquare();
maybeCalcNearbyRain();
@ -557,16 +554,17 @@
document.getElementById('btnLoad').addEventListener('click', async ()=>{
const sel = document.getElementById('stationSelect');
if (!sel || !sel.value) return;
const alias = sel.options[sel.selectedIndex].textContent || sel.value;
const aliasText = sel.options[sel.selectedIndex].textContent || sel.value;
const aliasParam = sel.value;
gZ = Number(sel.options[sel.selectedIndex].dataset.z || 0);
gY = Number(sel.options[sel.selectedIndex].dataset.y || 0);
gX = Number(sel.options[sel.selectedIndex].dataset.x || 0);
const lat = Number(sel.options[sel.selectedIndex].dataset.lat);
const lon = Number(sel.options[sel.selectedIndex].dataset.lon);
gStLat = isNaN(lat)? null : lat; gStLon = isNaN(lon)? null : lon;
gAlias = alias;
gAlias = aliasParam;
document.getElementById('rt_zyx').textContent = `z=${gZ}, y=${gY}, x=${gX}`;
try { await loadRealtime(alias); } catch (e) { console.warn(e); }
try { await loadRealtime(aliasParam); } catch (e) { console.warn(e); }
if (gZ && gY && gX) {
const from = fromDTLocalInput(document.getElementById('tsStart').value);
const to = fromDTLocalInput(document.getElementById('tsEnd').value);
@ -678,7 +676,7 @@
<div>下行短波:<span id="rt_dswrf"></span> W/m²</div>
<div>气压:<span id="rt_p"></span> Pa</div>
</div>
<div id="rt_stale" class="mt-2 hidden p-2 text-sm text-yellow-800 bg-yellow-50 border border-yellow-200 rounded">提示:该瓦片时次的就近实况相差超过 2 小时</div>
</div>
<div class="card my-4">