diff --git a/internal/database/radar_tiles.go b/internal/database/radar_tiles.go index f0357b0..d240acb 100644 --- a/internal/database/radar_tiles.go +++ b/internal/database/radar_tiles.go @@ -61,3 +61,18 @@ func UpsertRadarTile(ctx context.Context, db *sql.DB, product string, dt time.Ti } 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 +} diff --git a/internal/radar/scheduler.go b/internal/radar/scheduler.go index 373c5e5..908db57 100644 --- a/internal/radar/scheduler.go +++ b/internal/radar/scheduler.go @@ -77,15 +77,17 @@ func Start(ctx context.Context, opts Options) error { loc = time.FixedZone("CST", 8*3600) } - // 先立即执行一次(不延迟),随后每10分钟执行 + // 先立即执行一次(不延迟):拉取一次瓦片并抓取一次彩云实况 go func() { if err := runOnceFromNMC(ctx, opts); err != nil { log.Printf("[radar] first run error: %v", err) } }() - // 改为10分钟固定轮询一次 NMC 接口,解析时间并下载对应CMA瓦片 + // 瓦片:每3分钟查询一次 + go loop3(ctx, loc, opts) + // 实况:每10分钟一次 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 } @@ -107,8 +109,33 @@ func loop10(ctx context.Context, loc *time.Location, opts Options) { timer.Stop() return case <-timer.C: - if err := runOnceFromNMC(ctx, opts); err != nil { - log.Printf("[radar] runOnce error: %v", err) + if err := runRealtimeFromCaiyun(ctx); err != nil { + 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. 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 api := "https://www.nmc.cn/rest/weather?stationid=Wqsps" 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 { log.Printf("[radar] realtime(NN) failed: %v", err) } if err := fetchAndStoreRadarRealtimeFor(ctx, "广州雷达站", 23.146400, 113.341200); err != nil { log.Printf("[radar] realtime(GZ) failed: %v", err) } - // 新增海珠雷达站(使用与广州相同的瓦片,但坐标不同) if err := fetchAndStoreRadarRealtimeFor(ctx, "海珠雷达站", 23.090000, 113.350000); err != nil { log.Printf("[radar] realtime(HAIZHU) failed: %v", err) } - // 新增番禺雷达站 if err := fetchAndStoreRadarRealtimeFor(ctx, "番禺雷达站", 23.022500, 113.331300); err != nil { log.Printf("[radar] realtime(PANYU) failed: %v", err) } - // 并对 stations 表中符合条件(WH65LP 且有非零经纬度)的站点,抓取彩云实况并写入 radar_weather,alias=station_id - // 这样 radar_weather 可同时承载“雷达站别名”和“具体设备 station_id”两类记录。 - func() { - // 预先检查 token,避免对每个站点重复报错 - token := os.Getenv("CAIYUN_TOKEN") - if token == "" { - token = config.GetConfig().Forecast.CaiyunToken + // WH65LP 设备批量 + token := os.Getenv("CAIYUN_TOKEN") + if token == "" { + token = config.GetConfig().Forecast.CaiyunToken + } + if token == "" { + log.Printf("[radar] skip station realtime: missing CAIYUN_TOKEN") + return nil + } + coords, err := database.ListWH65LPStationsWithLatLon(ctx, database.GetDB()) + if err != nil { + log.Printf("[radar] list WH65LP stations failed: %v", err) + return nil + } + for _, s := range coords { + if err := fetchAndStoreRadarRealtimeFor(ctx, s.StationID, s.Lat, s.Lon); err != nil { + log.Printf("[radar] realtime(station=%s) failed: %v", s.StationID, err) } - if token == "" { - log.Printf("[radar] skip station realtime: missing CAIYUN_TOKEN") - return - } - coords, err := database.ListWH65LPStationsWithLatLon(ctx, database.GetDB()) - if err != nil { - log.Printf("[radar] list WH65LP stations failed: %v", err) - return - } - for _, s := range coords { - if err := fetchAndStoreRadarRealtimeFor(ctx, s.StationID, s.Lat, s.Lon); err != nil { - log.Printf("[radar] realtime(station=%s) failed: %v", s.StationID, err) - } - } - }() + } return nil } 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) + // 若数据库已有该瓦片则跳过 + 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")) dest := filepath.Join(opts.OutputDir, fnameOut) if _, err := os.Stat(dest); err == nil { diff --git a/templates/_header.html b/templates/_header.html index 3035190..d87e8af 100644 --- a/templates/_header.html +++ b/templates/_header.html @@ -3,7 +3,7 @@

{{ .Title }}