package server import ( "encoding/json" "fmt" "html/template" "log" "net/http" "os" "path/filepath" "sort" "strconv" "strings" "time" "weatherstation/internal/config" "weatherstation/internal/database" "weatherstation/pkg/types" "github.com/gin-gonic/gin" ) var staticBaseDir string // StartGinServer 启动Gin Web服务器 func StartGinServer() error { // 设置Gin模式 gin.SetMode(gin.ReleaseMode) // 创建Gin引擎 r := gin.Default() // 以可执行文件所在目录为基准查找资源(优先),其次回退到相对路径 exe, _ := os.Executable() exeDir := filepath.Dir(exe) // 模板目录 candidatesTpl := []string{ filepath.Join(exeDir, "templates", "*"), filepath.Join(exeDir, "..", "templates", "*"), filepath.Join("templates", "*"), filepath.Join("..", "templates", "*"), } var tplGlob string for _, c := range candidatesTpl { base := filepath.Dir(c) if st, err := os.Stat(base); err == nil && st.IsDir() { tplGlob = c break } } if tplGlob == "" { tplGlob = filepath.Join("templates", "*") } r.LoadHTMLGlob(tplGlob) // 静态目录 candidatesStatic := []string{ filepath.Join(exeDir, "static"), filepath.Join(exeDir, "..", "static"), "./static", "../static", } staticDir := candidatesStatic[0] for _, c := range candidatesStatic { if st, err := os.Stat(c); err == nil && st.IsDir() { staticDir = c break } } staticBaseDir = staticDir r.Static("/static", staticDir) // 前端SPA(Angular)静态资源与路由回退 // 构建产物目录(可执行目录优先) r.GET("/ui/*filepath", func(c *gin.Context) { // 物理文件优先,否则回退到 index.html(支持前端路由) requested := c.Param("filepath") if requested == "" || requested == "/" { // index.html candidates := []string{ filepath.Join(exeDir, "core/frontend/dist/ui/index.html"), filepath.Join(exeDir, "..", "core/frontend/dist/ui/index.html"), "./core/frontend/dist/ui/index.html", "../core/frontend/dist/ui/index.html", } for _, p := range candidates { if _, err := os.Stat(p); err == nil { c.File(p) return } } c.String(http.StatusNotFound, "ui not found") return } // 选择 baseDir baseDirCandidates := []string{ filepath.Join(exeDir, "core/frontend/dist/ui"), filepath.Join(exeDir, "..", "core/frontend/dist/ui"), "./core/frontend/dist/ui", "../core/frontend/dist/ui", } baseDir := baseDirCandidates[0] for _, d := range baseDirCandidates { if st, err := os.Stat(d); err == nil && st.IsDir() { baseDir = d break } } full := baseDir + requested if _, err := os.Stat(full); err == nil { c.File(full) return } c.File(filepath.Join(baseDir, "index.html")) }) // 路由设置 r.GET("/", indexHandler) r.GET("/radar/nanning", radarNanningHandler) r.GET("/radar/guangzhou", radarGuangzhouHandler) r.GET("/radar/panyu", radarPanyuHandler) r.GET("/radar/haizhu", radarHaizhuHandler) r.GET("/radar/imdroid", imdroidRadarHandler) // API路由组 api := r.Group("/api") { api.GET("/system/status", systemStatusHandler) api.GET("/stations", getStationsHandler) api.GET("/data", getDataHandler) api.GET("/forecast", getForecastHandler) api.GET("/radar/latest", latestRadarTileHandler) api.GET("/radar/at", radarTileAtHandler) api.GET("/radar/nearest", nearestRadarTileHandler) api.GET("/radar/times", radarTileTimesHandler) // multi-tiles at same dt api.GET("/radar/tiles_at", radarTilesAtHandler) api.GET("/radar/weather_latest", latestRadarWeatherHandler) api.GET("/radar/weather_at", radarWeatherAtHandler) api.GET("/radar/weather_aliases", radarWeatherAliasesHandler) api.GET("/radar/aliases", radarConfigAliasesHandler) api.GET("/radar/weather_nearest", radarWeatherNearestHandler) // Rain CMPA hourly tiles api.GET("/rain/latest", latestRainTileHandler) api.GET("/rain/at", rainTileAtHandler) api.GET("/rain/nearest", nearestRainTileHandler) api.GET("/rain/times", rainTileTimesHandler) api.GET("/rain/tiles_at", rainTilesAtHandler) } // 获取配置的Web端口 port := config.GetConfig().Server.WebPort if port == 0 { port = 10003 // 默认端口 } // 启动服务器 fmt.Printf("Gin Web服务器启动,监听端口 %d...\n", port) return r.Run(fmt.Sprintf(":%d", port)) } // indexHandler 处理主页请求 func indexHandler(c *gin.Context) { data := types.PageData{ Title: "英卓气象站", ServerTime: time.Now().Format("2006-01-02 15:04:05"), OnlineDevices: database.GetOnlineDevicesCount(database.GetDB()), TiandituKey: "0c260b8a094a4e0bc507808812cefdac", KmlLayersJSON: buildKmlLayersJSON(), } c.HTML(http.StatusOK, "index.html", data) } // radarNanningHandler 南宁雷达站占位页 func radarNanningHandler(c *gin.Context) { data := types.PageData{ Title: "雷达页面", ServerTime: time.Now().Format("2006-01-02 15:04:05"), OnlineDevices: database.GetOnlineDevicesCount(database.GetDB()), TiandituKey: "0c260b8a094a4e0bc507808812cefdac", KmlLayersJSON: buildKmlLayersJSON(), } c.HTML(http.StatusOK, "imdroid_radar.html", data) } // radarGuangzhouHandler 广州雷达站占位页 func radarGuangzhouHandler(c *gin.Context) { data := types.PageData{ Title: "雷达页面", ServerTime: time.Now().Format("2006-01-02 15:04:05"), OnlineDevices: database.GetOnlineDevicesCount(database.GetDB()), TiandituKey: "0c260b8a094a4e0bc507808812cefdac", KmlLayersJSON: buildKmlLayersJSON(), } c.HTML(http.StatusOK, "imdroid_radar.html", data) } // radarHaizhuHandler 海珠雷达站占位页 func radarHaizhuHandler(c *gin.Context) { data := types.PageData{ Title: "雷达页面", ServerTime: time.Now().Format("2006-01-02 15:04:05"), OnlineDevices: database.GetOnlineDevicesCount(database.GetDB()), TiandituKey: "0c260b8a094a4e0bc507808812cefdac", KmlLayersJSON: buildKmlLayersJSON(), } c.HTML(http.StatusOK, "imdroid_radar.html", data) } // radarPanyuHandler 番禺雷达站占位页 func radarPanyuHandler(c *gin.Context) { data := types.PageData{ Title: "雷达页面", ServerTime: time.Now().Format("2006-01-02 15:04:05"), OnlineDevices: database.GetOnlineDevicesCount(database.GetDB()), TiandituKey: "0c260b8a094a4e0bc507808812cefdac", KmlLayersJSON: buildKmlLayersJSON(), } c.HTML(http.StatusOK, "imdroid_radar.html", data) } func imdroidRadarHandler(c *gin.Context) { data := types.PageData{ Title: "英卓雷达站", ServerTime: time.Now().Format("2006-01-02 15:04:05"), OnlineDevices: database.GetOnlineDevicesCount(database.GetDB()), TiandituKey: "0c260b8a094a4e0bc507808812cefdac", KmlLayersJSON: buildKmlLayersJSON(), } c.HTML(http.StatusOK, "imdroid_radar.html", data) } func buildKmlLayersJSON() template.JS { layers := loadKmlLayers() if len(layers) == 0 { return template.JS("[]") } bytes, err := json.Marshal(layers) if err != nil { log.Printf("序列化KML列表失败: %v", err) return template.JS("[]") } return template.JS(bytes) } func loadKmlLayers() []types.KmlLayer { if staticBaseDir == "" { return nil } kmlDir := filepath.Join(staticBaseDir, "kml") entries, err := os.ReadDir(kmlDir) if err != nil { return nil } layers := make([]types.KmlLayer, 0, len(entries)) for _, entry := range entries { if entry.IsDir() { continue } name := entry.Name() if !strings.HasSuffix(strings.ToLower(name), ".kml") { continue } display := strings.TrimSuffix(name, filepath.Ext(name)) layers = append(layers, types.KmlLayer{ Name: display, URL: "/static/kml/" + name, }) } sort.Slice(layers, func(i, j int) bool { return strings.ToLower(layers[i].Name) < strings.ToLower(layers[j].Name) }) return layers } // systemStatusHandler 处理系统状态API请求 func systemStatusHandler(c *gin.Context) { status := types.SystemStatus{ OnlineDevices: database.GetOnlineDevicesCount(database.GetDB()), ServerTime: time.Now().Format("2006-01-02 15:04:05"), } c.JSON(http.StatusOK, status) } // getStationsHandler 处理获取站点列表API请求 func getStationsHandler(c *gin.Context) { stations, err := database.GetStations(database.GetDB()) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "查询站点失败"}) return } // 为每个站点计算十进制ID for i := range stations { if len(stations[i].StationID) > 6 { hexID := stations[i].StationID[len(stations[i].StationID)-6:] if decimalID, err := strconv.ParseInt(hexID, 16, 64); err == nil { stations[i].DecimalID = strconv.FormatInt(decimalID, 10) } } } c.JSON(http.StatusOK, stations) } // getDataHandler 处理获取历史数据API请求 func getDataHandler(c *gin.Context) { // 获取查询参数 decimalID := c.Query("decimal_id") startTime := c.Query("start_time") endTime := c.Query("end_time") interval := c.Query("interval") // 将十进制ID转换为十六进制(补足6位) decimalNum, err := strconv.ParseInt(decimalID, 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "无效的站点编号"}) return } hexID := fmt.Sprintf("%06X", decimalNum) stationID := fmt.Sprintf("RS485-%s", hexID) // 解析时间(按本地CST解析,避免被当作UTC) loc, _ := time.LoadLocation("Asia/Shanghai") if loc == nil { loc = time.FixedZone("CST", 8*3600) } start, err := time.ParseInLocation("2006-01-02 15:04:05", startTime, loc) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "无效的开始时间"}) return } end, err := time.ParseInLocation("2006-01-02 15:04:05", endTime, loc) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "无效的结束时间"}) return } // 获取数据(改为基于10分钟聚合表的再聚合) var points []types.WeatherPoint if interval == "raw" { points, err = database.GetSeriesRaw(database.GetDB(), stationID, start, end) } else { points, err = database.GetSeriesFrom10Min(database.GetDB(), stationID, start, end, interval) } if err != nil { log.Printf("查询数据失败: %v", err) // 记录具体错误到服务端日志 c.JSON(http.StatusInternalServerError, gin.H{ "error": fmt.Sprintf("查询数据失败: %v", err), }) return } c.JSON(http.StatusOK, points) } // getForecastHandler 处理获取预报数据API请求 func getForecastHandler(c *gin.Context) { // 获取查询参数 stationID := c.Query("station_id") startTime := c.Query("from") endTime := c.Query("to") provider := c.Query("provider") versionsStr := c.DefaultQuery("versions", "1") versions, _ := strconv.Atoi(versionsStr) if versions <= 0 { versions = 1 } if stationID == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "缺少station_id参数"}) return } // 如果没有提供时间范围,则默认查询未来3小时 loc, _ := time.LoadLocation("Asia/Shanghai") if loc == nil { loc = time.FixedZone("CST", 8*3600) } var start, end time.Time var err error if startTime == "" || endTime == "" { // 默认查询未来3小时 now := time.Now().In(loc) start = now.Truncate(time.Hour).Add(1 * time.Hour) // 下一个整点开始 end = start.Add(3 * time.Hour) // 未来3小时 } else { // 解析用户提供的时间 start, err = time.ParseInLocation("2006-01-02 15:04:05", startTime, loc) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "无效的开始时间格式"}) return } end, err = time.ParseInLocation("2006-01-02 15:04:05", endTime, loc) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "无效的结束时间格式"}) return } } // 获取预报数据 log.Printf("查询预报数据: stationID=%s, provider=%s, versions=%d, start=%s, end=%s", stationID, provider, versions, start.Format("2006-01-02 15:04:05"), end.Format("2006-01-02 15:04:05")) points, err := database.GetForecastData(database.GetDB(), stationID, start, end, provider, versions) if err != nil { log.Printf("查询预报数据失败: %v", err) c.JSON(http.StatusInternalServerError, gin.H{ "error": fmt.Sprintf("查询预报数据失败: %v", err), }) return } log.Printf("查询到预报数据: %d 条", len(points)) c.JSON(http.StatusOK, points) }