feat: 缩短瓦片下载周期时间

This commit is contained in:
yarnom 2025-09-25 11:17:09 +08:00
parent d0f96710e0
commit 0449971bcb
4 changed files with 94 additions and 34 deletions

View File

@ -61,3 +61,18 @@ func UpsertRadarTile(ctx context.Context, db *sql.DB, product string, dt time.Ti
} }
return nil return nil
} }
// HasRadarTile reports whether a radar tile exists for the given key.
// It checks by (product, dt, z, y, x) in table `radar_tiles`.
func HasRadarTile(ctx context.Context, db *sql.DB, product string, dt time.Time, z, y, x int) (bool, error) {
const q = `SELECT 1 FROM radar_tiles WHERE product=$1 AND dt=$2 AND z=$3 AND y=$4 AND x=$5 LIMIT 1`
var one int
err := db.QueryRowContext(ctx, q, product, dt, z, y, x).Scan(&one)
if err == sql.ErrNoRows {
return false, nil
}
if err != nil {
return false, fmt.Errorf("check radar tile exists: %w", err)
}
return true, nil
}

View File

@ -77,15 +77,17 @@ func Start(ctx context.Context, opts Options) error {
loc = time.FixedZone("CST", 8*3600) loc = time.FixedZone("CST", 8*3600)
} }
// 先立即执行一次(不延迟)随后每10分钟执行 // 先立即执行一次(不延迟):拉取一次瓦片并抓取一次彩云实况
go func() { go func() {
if err := runOnceFromNMC(ctx, opts); err != nil { if err := runOnceFromNMC(ctx, opts); err != nil {
log.Printf("[radar] first run error: %v", err) log.Printf("[radar] first run error: %v", err)
} }
}() }()
// 改为10分钟固定轮询一次 NMC 接口解析时间并下载对应CMA瓦片 // 瓦片每3分钟查询一次
go loop3(ctx, loc, opts)
// 实况每10分钟一次
go loop10(ctx, loc, opts) go loop10(ctx, loc, opts)
log.Printf("[radar] scheduler started (interval=10m, dir=%s, tile=%d/%d/%d)", opts.OutputDir, opts.Z, opts.Y, opts.X) log.Printf("[radar] scheduler started (tiles=3m, realtime=10m, dir=%s, tile=%d/%d/%d)", opts.OutputDir, opts.Z, opts.Y, opts.X)
return nil return nil
} }
@ -107,8 +109,33 @@ func loop10(ctx context.Context, loc *time.Location, opts Options) {
timer.Stop() timer.Stop()
return return
case <-timer.C: case <-timer.C:
if err := runOnceFromNMC(ctx, opts); err != nil { if err := runRealtimeFromCaiyun(ctx); err != nil {
log.Printf("[radar] runOnce error: %v", err) log.Printf("[radar] realtime run error: %v", err)
}
}
}
}
// 每3分钟的瓦片轮询
func loop3(ctx context.Context, loc *time.Location, opts Options) {
for {
if ctx.Err() != nil {
return
}
now := time.Now().In(loc)
runAt := roundDownN(now, 3*time.Minute).Add(3 * time.Minute)
sleep := time.Until(runAt)
if sleep < 0 {
sleep = 0
}
timer := time.NewTimer(sleep)
select {
case <-ctx.Done():
timer.Stop()
return
case <-timer.C:
if err := runTilesFromNMC(ctx, opts); err != nil {
log.Printf("[radar] tiles run error: %v", err)
} }
} }
} }
@ -258,6 +285,15 @@ var reDigits17 = regexp.MustCompile(`([0-9]{17})`)
// runOnceFromNMC fetches NMC JSON, extracts timestamp, shifts +8h, then downloads CMA tile for opts.Z/Y/X. // runOnceFromNMC fetches NMC JSON, extracts timestamp, shifts +8h, then downloads CMA tile for opts.Z/Y/X.
func runOnceFromNMC(ctx context.Context, opts Options) error { func runOnceFromNMC(ctx context.Context, opts Options) error {
// 保留原语义:两者都执行
if err := runTilesFromNMC(ctx, opts); err != nil {
return err
}
return runRealtimeFromCaiyun(ctx)
}
// 仅瓦片下载:查询 NMC解析时间按该时刻下载 CMA 瓦片若DB已存在则跳过
func runTilesFromNMC(ctx context.Context, opts Options) error {
// 1) Fetch NMC JSON // 1) Fetch NMC JSON
api := "https://www.nmc.cn/rest/weather?stationid=Wqsps" api := "https://www.nmc.cn/rest/weather?stationid=Wqsps"
client := &http.Client{Timeout: 15 * time.Second} client := &http.Client{Timeout: 15 * time.Second}
@ -334,50 +370,59 @@ func runOnceFromNMC(ctx context.Context, opts Options) error {
} }
} }
// Also fetch realtime weather for radar stations every 10 minutes return nil
}
// 仅彩云实况10分钟一次
func runRealtimeFromCaiyun(ctx context.Context) error {
if err := fetchAndStoreRadarRealtimeFor(ctx, "南宁雷达站", 23.097234, 108.715433); err != nil { if err := fetchAndStoreRadarRealtimeFor(ctx, "南宁雷达站", 23.097234, 108.715433); err != nil {
log.Printf("[radar] realtime(NN) failed: %v", err) log.Printf("[radar] realtime(NN) failed: %v", err)
} }
if err := fetchAndStoreRadarRealtimeFor(ctx, "广州雷达站", 23.146400, 113.341200); err != nil { if err := fetchAndStoreRadarRealtimeFor(ctx, "广州雷达站", 23.146400, 113.341200); err != nil {
log.Printf("[radar] realtime(GZ) failed: %v", err) log.Printf("[radar] realtime(GZ) failed: %v", err)
} }
// 新增海珠雷达站(使用与广州相同的瓦片,但坐标不同)
if err := fetchAndStoreRadarRealtimeFor(ctx, "海珠雷达站", 23.090000, 113.350000); err != nil { if err := fetchAndStoreRadarRealtimeFor(ctx, "海珠雷达站", 23.090000, 113.350000); err != nil {
log.Printf("[radar] realtime(HAIZHU) failed: %v", err) log.Printf("[radar] realtime(HAIZHU) failed: %v", err)
} }
// 新增番禺雷达站
if err := fetchAndStoreRadarRealtimeFor(ctx, "番禺雷达站", 23.022500, 113.331300); err != nil { if err := fetchAndStoreRadarRealtimeFor(ctx, "番禺雷达站", 23.022500, 113.331300); err != nil {
log.Printf("[radar] realtime(PANYU) failed: %v", err) log.Printf("[radar] realtime(PANYU) failed: %v", err)
} }
// 并对 stations 表中符合条件WH65LP 且有非零经纬度)的站点,抓取彩云实况并写入 radar_weatheralias=station_id // WH65LP 设备批量
// 这样 radar_weather 可同时承载“雷达站别名”和“具体设备 station_id”两类记录。
func() {
// 预先检查 token避免对每个站点重复报错
token := os.Getenv("CAIYUN_TOKEN") token := os.Getenv("CAIYUN_TOKEN")
if token == "" { if token == "" {
token = config.GetConfig().Forecast.CaiyunToken token = config.GetConfig().Forecast.CaiyunToken
} }
if token == "" { if token == "" {
log.Printf("[radar] skip station realtime: missing CAIYUN_TOKEN") log.Printf("[radar] skip station realtime: missing CAIYUN_TOKEN")
return return nil
} }
coords, err := database.ListWH65LPStationsWithLatLon(ctx, database.GetDB()) coords, err := database.ListWH65LPStationsWithLatLon(ctx, database.GetDB())
if err != nil { if err != nil {
log.Printf("[radar] list WH65LP stations failed: %v", err) log.Printf("[radar] list WH65LP stations failed: %v", err)
return return nil
} }
for _, s := range coords { for _, s := range coords {
if err := fetchAndStoreRadarRealtimeFor(ctx, s.StationID, s.Lat, s.Lon); err != nil { if err := fetchAndStoreRadarRealtimeFor(ctx, s.StationID, s.Lat, s.Lon); err != nil {
log.Printf("[radar] realtime(station=%s) failed: %v", s.StationID, err) log.Printf("[radar] realtime(station=%s) failed: %v", s.StationID, err)
} }
} }
}()
return nil return nil
} }
func downloadAndStoreTile(ctx context.Context, local time.Time, dateStr, hh, mm string, z, y, x int, opts Options) error { func downloadAndStoreTile(ctx context.Context, local time.Time, dateStr, hh, mm string, z, y, x int, opts Options) error {
url := fmt.Sprintf("https://image.data.cma.cn/tiles/China/RADAR_L3_MST_CREF_GISJPG_Tiles_CR/%s/%s/%s/%d/%d/%d.bin", dateStr, hh, mm, z, y, x) url := fmt.Sprintf("https://image.data.cma.cn/tiles/China/RADAR_L3_MST_CREF_GISJPG_Tiles_CR/%s/%s/%s/%d/%d/%d.bin", dateStr, hh, mm, z, y, x)
// 若数据库已有该瓦片则跳过
if ref, err := ParseCMATileURL(url); err == nil {
exists, err := database.HasRadarTile(ctx, database.GetDB(), ref.Product, ref.DT, ref.Z, ref.Y, ref.X)
if err != nil {
return err
}
if exists {
log.Printf("[radar] skip existing tile in DB: %s %s z=%d y=%d x=%d", ref.Product, ref.DT.Format("2006-01-02 15:04"), ref.Z, ref.Y, ref.X)
return nil
}
}
fnameOut := fmt.Sprintf("radar_z%d_y%d_x%d_%s.bin", z, y, x, local.Format("20060102_1504")) fnameOut := fmt.Sprintf("radar_z%d_y%d_x%d_%s.bin", z, y, x, local.Format("20060102_1504"))
dest := filepath.Join(opts.OutputDir, fnameOut) dest := filepath.Join(opts.OutputDir, fnameOut)
if _, err := os.Stat(dest); err == nil { if _, err := os.Stat(dest); err == nil {

View File

@ -3,7 +3,7 @@
<div class="content-narrow mx-auto px-4 py-2 flex items-center justify-between flex-wrap gap-2"> <div class="content-narrow mx-auto px-4 py-2 flex items-center justify-between flex-wrap gap-2">
<h1 class="text-xl md:text-2xl font-semibold">{{ .Title }}</h1> <h1 class="text-xl md:text-2xl font-semibold">{{ .Title }}</h1>
<nav class="text-sm flex items-center gap-3"> <nav class="text-sm flex items-center gap-3">
<a href="/" class="text-blue-600 hover:text-blue-700">首页</a> <a href="/" class="text-blue-600 hover:text-blue-700">英卓气象站</a>
<a href="/radar/imdroid" class="text-blue-600 hover:text-blue-700">英卓雷达站</a> <a href="/radar/imdroid" class="text-blue-600 hover:text-blue-700">英卓雷达站</a>
<!-- <a href="/radar/nanning" class="text-blue-600 hover:text-blue-700">南宁雷达站</a>--> <!-- <a href="/radar/nanning" class="text-blue-600 hover:text-blue-700">南宁雷达站</a>-->
<!-- <a href="/radar/guangzhou" class="text-blue-600 hover:text-blue-700">广州雷达站</a>--> <!-- <a href="/radar/guangzhou" class="text-blue-600 hover:text-blue-700">广州雷达站</a>-->

View File

@ -339,7 +339,7 @@
document.getElementById('sectorDBZ').textContent=Number(best.dbz).toFixed(1); document.getElementById('sectorDBZ').textContent=Number(best.dbz).toFixed(1);
statusEl.textContent='三小时内可能有降雨≥40 dBZ '; detailEl.classList.remove('hidden'); statusEl.textContent='三小时内可能有降雨≥40 dBZ '; detailEl.classList.remove('hidden');
} }
}catch(e){ document.getElementById('sectorStatus').textContent='风险评估计算失败:'+e.message; } }catch(e){ document.getElementById('sectorStatus').textContent='计算失败:'+e.message; }
} }
function maybePlotSquare(){ function maybePlotSquare(){
@ -374,7 +374,7 @@
xsFan.push(gStLon); ysFan.push(gStLat); xsFan.push(gStLon); ysFan.push(gStLat);
data.push({ type:'scatter', mode:'lines', x:xsFan, y:ysFan, line:{color:'#FFFFFF',width:2,dash:'dash'}, fill:'toself', fillcolor:'rgba(255,255,255,0.18)', hoverinfo:'skip', showlegend:false }); data.push({ type:'scatter', mode:'lines', x:xsFan, y:ysFan, line:{color:'#FFFFFF',width:2,dash:'dash'}, fill:'toself', fillcolor:'rgba(255,255,255,0.18)', hoverinfo:'skip', showlegend:false });
} }
// 叠加附近5km圆 // 叠加附近8km圆
{ {
const Rm = 8000; const samples=128; const xsC=[], ysC=[]; const Rm = 8000; const samples=128; const xsC=[], ysC=[];
for(let i=0;i<=samples;i++){ const θ=i*(360/samples); const p=destPoint(gStLat,gStLon,θ,Rm); xsC.push(p.lon); ysC.push(p.lat); } for(let i=0;i<=samples;i++){ const θ=i*(360/samples); const p=destPoint(gStLat,gStLon,θ,Rm); xsC.push(p.lon); ysC.push(p.lat); }
@ -538,7 +538,7 @@
<div>时间:<span id="rt_dt"></span></div> <div>时间:<span id="rt_dt"></span></div>
<div>位置:<span id="rt_lat"></span><span id="rt_lon"></span></div> <div>位置:<span id="rt_lat"></span><span id="rt_lon"></span></div>
<div>温度:<span id="rt_t"></span></div> <div>温度:<span id="rt_t"></span></div>
<div>湿度:<span id="rt_h"></span></div> <div>湿度:<span id="rt_h"></span> %</div>
<div>风速:<span id="rt_ws"></span> m/s</div> <div>风速:<span id="rt_ws"></span> m/s</div>
<div>风向:<span id="rt_wd"></span> °</div> <div>风向:<span id="rt_wd"></span> °</div>
<div>云量:<span id="rt_c"></span> %</div> <div>云量:<span id="rt_c"></span> %</div>