2025-11-03 15:45:49 +08:00

507 lines
15 KiB
Go
Raw Permalink 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 server
import (
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"time"
"weatherstation/internal/config"
"weatherstation/internal/database"
"weatherstation/pkg/types"
"github.com/gin-gonic/gin"
)
// newGinEngine 统一创建和配置 Gin 引擎(模板/静态/路由)
func newGinEngine() *gin.Engine {
// 设置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
}
}
r.Static("/static", staticDir)
// 前端SPAAngular静态资源与路由回退
// 构建产物目录(可执行目录优先)
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("/bigscreen", bigscreenHandler)
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)
}
return r
}
// StartGinServer 启动Gin Web服务器配置端口
func StartGinServer() error {
r := newGinEngine()
// 获取配置的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))
}
// StartGinServerOn 在指定端口启动一个独立的 Gin 实例
func StartGinServerOn(port int) error {
r := newGinEngine()
if port <= 0 {
port = 10008
}
fmt.Printf("Gin Web服务器启动监听端口 %d...\n", port)
return r.Run(fmt.Sprintf(":%d", port))
}
// StartBigscreenServerOn 在指定端口启动以大屏为根路径的 Gin 实例(/ -> bigscreen
func StartBigscreenServerOn(port int) error {
// 独立构建一个以大屏为根路径的引擎,避免与普通引擎重复注册根路由
gin.SetMode(gin.ReleaseMode)
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
}
}
r.Static("/static", staticDir)
// SPA 资源
r.GET("/ui/*filepath", func(c *gin.Context) {
requested := c.Param("filepath")
if requested == "" || requested == "/" {
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
}
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("/", bigscreenHandler)
// 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)
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)
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)
}
if port <= 0 {
port = 10008
}
fmt.Printf("Gin Bigscreen 服务器启动,监听端口 %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",
}
c.HTML(http.StatusOK, "index.html", data)
}
// bigscreenHandler 大屏演示页
func bigscreenHandler(c *gin.Context) {
data := types.PageData{
Title: "英卓大屏",
ServerTime: time.Now().Format("2006-01-02 15:04:05"),
OnlineDevices: database.GetOnlineDevicesCount(database.GetDB()),
TiandituKey: "0c260b8a094a4e0bc507808812cefdac",
}
c.HTML(http.StatusOK, "bigscreen.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",
}
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",
}
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",
}
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",
}
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",
}
c.HTML(http.StatusOK, "imdroid_radar.html", data)
}
// 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)
}