324 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"context"
"flag"
"log"
"os"
"sync"
"time"
"weatherstation/internal/config"
"weatherstation/internal/database"
"weatherstation/internal/forecast"
"weatherstation/internal/radar"
"weatherstation/internal/rain"
"weatherstation/internal/selftest"
"weatherstation/internal/server"
"weatherstation/internal/tools"
)
func main() {
// 命令行参数
var webOnly = flag.Bool("web", false, "只启动Web服务器Gin")
var udpOnly = flag.Bool("udp", false, "只启动UDP服务器")
// 调试回填10分钟表
var doBackfill = flag.Bool("backfill", false, "将16s原始数据聚合写入10分钟表调试用途")
var bfStation = flag.String("station", "", "指定站点ID为空则全站回填")
var bfFrom = flag.String("from", "", "回填起始时间格式YYYY-MM-DD HH:MM:SS")
var bfTo = flag.String("to", "", "回填结束时间格式YYYY-MM-DD HH:MM:SS")
var bfWrap = flag.Float64("wrap", 0, "回绕一圈对应毫米值mm<=0 则降级为仅计当前值")
// 自检控制
var noSelftest = flag.Bool("no-selftest", false, "跳过启动自检")
var selftestOnly = flag.Bool("selftest_only", false, "仅执行自检后退出")
// 预报抓取
var forecastOnly = flag.Bool("forecast_only", false, "仅执行一次open-meteo拉取并退出")
var caiyunOnly = flag.Bool("caiyun_only", false, "仅执行一次彩云拉取并退出")
var cmaCLI = flag.Bool("cma_cli", false, "仅执行一次CMA接口抓取并打印未来三小时")
var cmaOnly = flag.Bool("cma_only", false, "仅执行一次CMA拉取并退出")
var forecastDay = flag.String("forecast_day", "", "按日期抓取当天0点到当前时间+3h格式YYYY-MM-DD")
// 历史数据补完
var historicalOnly = flag.Bool("historical_only", false, "仅执行历史数据补完并退出")
var historicalStart = flag.String("historical_start", "", "历史数据开始日期格式YYYY-MM-DD")
var historicalEnd = flag.String("historical_end", "", "历史数据结束日期格式YYYY-MM-DD")
// 覆盖风:使用彩云实况替换导出中的风速/风向
var useWindOverride = flag.Bool("wind", false, "使用彩云实况覆盖导出CSV中的风速/风向")
// 历史CSV导出
var exportRangeOnly = flag.Bool("export_range", false, "按日期范围导出10分钟CSV含ZTD融合并退出。日期格式支持 YYYY-MM-DD 或 YYYYMMDD")
var exportStart = flag.String("export_start", "", "导出起始日期(含),格式 YYYY-MM-DD 或 YYYYMMDD")
var exportEnd = flag.String("export_end", "", "导出结束日期(含),格式 YYYY-MM-DD 或 YYYYMMDD")
// 雷达导入单个CMA瓦片到数据库
var importTile = flag.Bool("import_tile", false, "导入一个CMA雷达瓦片到数据库并退出")
var tileURL = flag.String("tile_url", "", "瓦片URL或/tiles/...路径用于解析product/时间/z/y/x")
var tilePath = flag.String("tile_path", "", "瓦片本地文件路径(.bin")
flag.Parse()
// 设置日志
server.SetupLogger()
// 初始化数据库连接
_ = database.GetDB() // 确保PostgreSQL连接已初始化
defer database.Close()
// 初始化MySQL连接如果配置存在
_ = database.GetMySQL()
defer database.CloseMySQL()
// 启动前自检
if !*noSelftest {
if err := selftest.Run(context.Background()); err != nil {
log.Fatalf("启动自检失败: %v", err)
}
if *selftestOnly {
log.Println("自检完成,按 --selftest_only 要求退出")
return
}
}
// 单次 open-meteo 拉取
if *forecastOnly {
if err := forecast.RunOpenMeteoFetch(context.Background()); err != nil {
log.Fatalf("open-meteo 拉取失败: %v", err)
}
log.Println("open-meteo 拉取完成")
return
}
// 单次 彩云 拉取token 从环境变量 CAIYUN_TOKEN 或命令行 -caiyun_token 读取)
if *caiyunOnly {
token := os.Getenv("CAIYUN_TOKEN")
if token == "" {
// 退回配置
token = config.GetConfig().Forecast.CaiyunToken
if token == "" {
log.Fatalf("未提供彩云 token请设置环境变量 CAIYUN_TOKEN 或配置文件 forecast.caiyun_token")
}
}
if err := forecast.RunCaiyunFetch(context.Background(), token); err != nil {
log.Fatalf("caiyun 拉取失败: %v", err)
}
log.Println("caiyun 拉取完成")
return
}
// 单次 CMA 拉取(固定参数)写库并退出
if *cmaOnly {
if err := forecast.RunCMAFetch(context.Background()); err != nil {
log.Fatalf("CMA 拉取失败: %v", err)
}
log.Println("CMA 拉取完成")
return
}
// 单次 CMA 拉取(固定参数)并打印三小时
if *cmaCLI {
if err := forecast.RunCMACLI(context.Background()); err != nil {
log.Fatalf("CMA 拉取失败: %v", err)
}
return
}
// 导入一个CMA雷达瓦片到数据库
if *importTile {
if *tileURL == "" || *tilePath == "" {
log.Fatalln("import_tile 需要提供 --tile_url 与 --tile_path")
}
if err := radar.ImportTileFile(context.Background(), *tileURL, *tilePath); err != nil {
log.Fatalf("导入雷达瓦片失败: %v", err)
}
log.Println("导入雷达瓦片完成")
return
}
// 历史CSV范围导出
if *exportRangeOnly {
if *exportStart == "" || *exportEnd == "" {
log.Fatalln("export_range 需要提供 --export_start 与 --export_end 日期YYYY-MM-DD 或 YYYYMMDD")
}
var opts tools.ExporterOptions
if *useWindOverride {
token := os.Getenv("CAIYUN_TOKEN")
if token == "" {
token = config.GetConfig().Forecast.CaiyunToken
}
if token == "" {
log.Println("警告: 指定了 --wind 但未提供彩云 token忽略风覆盖")
} else {
opts.OverrideWindWithCaiyun = true
opts.CaiyunToken = token
}
}
exporter := tools.NewExporterWithOptions(opts)
if err := exporter.ExportRange(context.Background(), *exportStart, *exportEnd); err != nil {
log.Fatalf("export_range 失败: %v", err)
}
log.Println("export_range 完成")
return
}
// 工具按日期抓取当天0点到当前时间+3小时两家
if *forecastDay != "" {
if err := tools.RunForecastFetchForDay(context.Background(), *forecastDay); err != nil {
log.Fatalf("forecast_day 运行失败: %v", err)
}
log.Println("forecast_day 完成")
return
}
// 历史数据补完
if *historicalOnly {
if *historicalStart == "" || *historicalEnd == "" {
log.Fatalln("历史数据补完需要提供 --historical_start 与 --historical_end 日期格式YYYY-MM-DD")
}
if err := forecast.RunOpenMeteoHistoricalFetch(context.Background(), *historicalStart, *historicalEnd); err != nil {
log.Fatalf("历史数据补完失败: %v", err)
}
log.Println("历史数据补完成")
return
}
// Backfill 调试路径
if *doBackfill {
if *bfFrom == "" || *bfTo == "" {
log.Fatalln("backfill 需要提供 --from 与 --to 时间")
}
loc, _ := time.LoadLocation("Asia/Shanghai")
if loc == nil {
loc = time.FixedZone("CST", 8*3600)
}
fromT, err := time.ParseInLocation("2006-01-02 15:04:05", *bfFrom, loc)
if err != nil {
log.Fatalf("解析from失败: %v", err)
}
toT, err := time.ParseInLocation("2006-01-02 15:04:05", *bfTo, loc)
if err != nil {
log.Fatalf("解析to失败: %v", err)
}
ctx := context.Background()
if err := tools.RunBackfill10Min(ctx, tools.BackfillOptions{
StationID: *bfStation,
FromTime: fromT,
ToTime: toT,
WrapCycleMM: *bfWrap,
BucketMinutes: 10,
}); err != nil {
log.Fatalf("回填失败: %v", err)
}
log.Println("回填完成")
return
}
// 根据命令行参数启动服务
startExporterBackground := func(wg *sync.WaitGroup) {
if wg != nil {
wg.Add(1)
}
go func() {
defer func() {
if wg != nil {
wg.Done()
}
}()
log.Println("启动数据导出器10分钟...")
ctx := context.Background()
// 处理 --wind 覆盖
var opts tools.ExporterOptions
if *useWindOverride {
token := os.Getenv("CAIYUN_TOKEN")
if token == "" {
token = config.GetConfig().Forecast.CaiyunToken
}
if token == "" {
log.Println("警告: 指定了 --wind 但未提供彩云 token忽略风覆盖")
} else {
opts.OverrideWindWithCaiyun = true
opts.CaiyunToken = token
}
}
exporter := tools.NewExporterWithOptions(opts)
if err := exporter.Start(ctx); err != nil {
log.Printf("导出器退出: %v", err)
}
}()
}
startRadarSchedulerBackground := func(wg *sync.WaitGroup) {
if wg != nil {
wg.Add(1)
}
go func() {
defer func() {
if wg != nil {
wg.Done()
}
}()
log.Println("启动雷达下载任务每10分钟无延迟固定瓦片 7/40/102...")
ctx := context.Background()
_ = radar.Start(ctx, radar.Options{StoreToDB: true, Z: 7, Y: 40, X: 102})
}()
}
startRainSchedulerBackground := func(wg *sync.WaitGroup) {
if wg != nil {
wg.Add(1)
}
go func() {
defer func() {
if wg != nil {
wg.Done()
}
}()
log.Println("启动一小时降雨下载任务每10分钟固定瓦片 7/40/102 与 7/40/104...")
ctx := context.Background()
_ = rain.Start(ctx, rain.Options{StoreToDB: true})
}()
}
if *webOnly {
// 只启动Web服务器 + 导出器
startExporterBackground(nil)
startRadarSchedulerBackground(nil)
startRainSchedulerBackground(nil)
log.Println("启动Web服务器模式...")
if err := server.StartGinServer(); err != nil {
log.Fatalf("启动Web服务器失败: %v", err)
}
} else if *udpOnly {
// 只启动UDP服务器 + 导出器
startExporterBackground(nil)
startRadarSchedulerBackground(nil)
startRainSchedulerBackground(nil)
log.Println("启动UDP服务器模式...")
if err := server.StartUDPServer(); err != nil {
log.Fatalf("启动UDP服务器失败: %v", err)
}
} else {
// 同时启动UDP和Web服务器 + 导出器
log.Println("启动完整模式UDP + Web服务器 + 导出器...")
var wg sync.WaitGroup
wg.Add(2)
// 启动UDP服务器
go func() {
defer wg.Done()
log.Println("正在启动UDP服务器...")
if err := server.StartUDPServer(); err != nil {
log.Printf("UDP服务器异常退出: %v", err)
}
}()
// 启动Web服务器
go func() {
defer wg.Done()
log.Println("正在启动Web服务器...")
if err := server.StartGinServer(); err != nil {
log.Printf("Web服务器异常退出: %v", err)
}
}()
startExporterBackground(&wg)
startRadarSchedulerBackground(&wg)
startRainSchedulerBackground(&wg)
wg.Wait()
}
}