250 lines
5.9 KiB
Go
250 lines
5.9 KiB
Go
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(×tamp, &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")
|
||
}
|