weather-station/gin_server.go
2025-08-07 20:12:28 +08:00

250 lines
5.9 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 (
"database/sql"
"fmt"
"net/http"
"time"
"weatherstation/config"
"github.com/gin-gonic/gin"
_ "github.com/lib/pq"
)
var ginDB *sql.DB
func initGinDB() error {
cfg := config.GetConfig()
connStr := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
cfg.Database.Host, cfg.Database.Port, cfg.Database.User,
cfg.Database.Password, cfg.Database.DBName, cfg.Database.SSLMode)
var err error
ginDB, err = sql.Open("postgres", connStr)
if err != nil {
return fmt.Errorf("无法连接到数据库: %v", err)
}
err = ginDB.Ping()
if err != nil {
return fmt.Errorf("数据库连接测试失败: %v", err)
}
return nil
}
// 获取在线设备数量
func getOnlineDevicesCount() int {
if ginDB == nil {
return 0
}
query := `
SELECT COUNT(DISTINCT station_id)
FROM rs485_weather_data
WHERE timestamp > NOW() - INTERVAL '5 minutes'`
var count int
err := ginDB.QueryRow(query).Scan(&count)
if err != nil {
return 0
}
return count
}
// 主页面处理器
func indexHandler(c *gin.Context) {
data := PageData{
Title: "英卓气象站",
ServerTime: time.Now().Format("2006-01-02 15:04:05"),
OnlineDevices: getOnlineDevicesCount(),
}
c.HTML(http.StatusOK, "index.html", data)
}
// 系统状态API
func systemStatusHandler(c *gin.Context) {
status := SystemStatus{
OnlineDevices: getOnlineDevicesCount(),
ServerTime: time.Now().Format("2006-01-02 15:04:05"),
}
c.JSON(http.StatusOK, status)
}
// 获取WH65LP站点列表API
func getStationsHandler(c *gin.Context) {
query := `
SELECT DISTINCT s.station_id,
COALESCE(s.password, '') as station_name,
'WH65LP' as device_type,
COALESCE(MAX(r.timestamp), '1970-01-01'::timestamp) as last_update
FROM stations s
LEFT JOIN rs485_weather_data r ON s.station_id = r.station_id
WHERE s.station_id LIKE 'RS485-%'
GROUP BY s.station_id, s.password
ORDER BY s.station_id`
rows, err := ginDB.Query(query)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询站点失败"})
return
}
defer rows.Close()
var stations []Station
for rows.Next() {
var station Station
var lastUpdate time.Time
err := rows.Scan(&station.StationID, &station.StationName, &station.DeviceType, &lastUpdate)
if err != nil {
continue
}
station.LastUpdate = lastUpdate.Format("2006-01-02 15:04:05")
stations = append(stations, station)
}
c.JSON(http.StatusOK, stations)
}
// 获取站点历史数据API
func getDataHandler(c *gin.Context) {
stationID := c.Query("station_id")
startTime := c.Query("start_time")
endTime := c.Query("end_time")
interval := c.DefaultQuery("interval", "1hour")
if stationID == "" || startTime == "" || endTime == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少必要参数"})
return
}
var intervalSQL string
switch interval {
case "10min":
intervalSQL = "10 minutes"
case "30min":
intervalSQL = "30 minutes"
case "1hour":
intervalSQL = "1 hour"
default:
intervalSQL = "1 hour"
}
// 构建查询SQL - 使用时间窗口聚合
query := fmt.Sprintf(`
WITH time_series AS (
SELECT
date_trunc('hour', timestamp) +
INTERVAL '%s' * FLOOR(EXTRACT(EPOCH FROM timestamp - date_trunc('hour', timestamp)) / EXTRACT(EPOCH FROM INTERVAL '%s')) as time_bucket,
temperature,
humidity,
pressure,
wind_speed,
wind_direction,
rainfall,
light,
uv
FROM rs485_weather_data
WHERE station_id = $1
AND timestamp >= $2::timestamp
AND timestamp <= $3::timestamp
),
aggregated_data AS (
SELECT
time_bucket,
ROUND(AVG(temperature)::numeric, 2) as temperature,
ROUND(AVG(humidity)::numeric, 2) as humidity,
ROUND(AVG(pressure)::numeric, 2) as pressure,
ROUND(AVG(wind_speed)::numeric, 2) as wind_speed,
-- 风向使用矢量平均
ROUND(DEGREES(ATAN2(
AVG(SIN(RADIANS(wind_direction))),
AVG(COS(RADIANS(wind_direction)))
))::numeric + CASE
WHEN DEGREES(ATAN2(
AVG(SIN(RADIANS(wind_direction))),
AVG(COS(RADIANS(wind_direction)))
)) < 0 THEN 360
ELSE 0
END, 2) as wind_direction,
-- 雨量使用差值计算
ROUND((MAX(rainfall) - MIN(rainfall))::numeric, 3) as rainfall_diff,
ROUND(AVG(light)::numeric, 2) as light,
ROUND(AVG(uv)::numeric, 2) as uv
FROM time_series
GROUP BY time_bucket
)
SELECT
time_bucket,
COALESCE(temperature, 0) as temperature,
COALESCE(humidity, 0) as humidity,
COALESCE(pressure, 0) as pressure,
COALESCE(wind_speed, 0) as wind_speed,
COALESCE(wind_direction, 0) as wind_direction,
COALESCE(rainfall_diff, 0) as rainfall,
COALESCE(light, 0) as light,
COALESCE(uv, 0) as uv
FROM aggregated_data
ORDER BY time_bucket`, intervalSQL, intervalSQL)
rows, err := ginDB.Query(query, stationID, startTime, endTime)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询数据失败"})
return
}
defer rows.Close()
var data []WeatherPoint
for rows.Next() {
var point WeatherPoint
var timestamp time.Time
err := rows.Scan(&timestamp, &point.Temperature, &point.Humidity,
&point.Pressure, &point.WindSpeed, &point.WindDir,
&point.Rainfall, &point.Light, &point.UV)
if err != nil {
continue
}
point.DateTime = timestamp.Format("2006-01-02 15:04:05")
data = append(data, point)
}
c.JSON(http.StatusOK, data)
}
func StartGinServer() {
err := initGinDB()
if err != nil {
fmt.Printf("初始化Gin数据库连接失败: %v\n", err)
return
}
// 设置Gin模式
gin.SetMode(gin.ReleaseMode)
// 创建Gin引擎
r := gin.Default()
// 加载HTML模板
r.LoadHTMLGlob("templates/*")
// 静态文件服务
r.Static("/static", "./static")
// 路由设置
r.GET("/", indexHandler)
// API路由组
api := r.Group("/api")
{
api.GET("/system/status", systemStatusHandler)
api.GET("/stations", getStationsHandler)
api.GET("/data", getDataHandler)
}
// 启动服务器
fmt.Println("Gin Web服务器启动监听端口 10003...")
r.Run(":10003")
}