weather-station/web_server.go

241 lines
6.0 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"
"encoding/json"
"fmt"
"log"
"net/http"
"path/filepath"
"strings"
"time"
"weatherstation/config"
_ "github.com/lib/pq"
)
var db *sql.DB
func initWebDB() 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
db, err = sql.Open("postgres", connStr)
if err != nil {
return fmt.Errorf("无法连接到数据库: %v", err)
}
err = db.Ping()
if err != nil {
return fmt.Errorf("数据库连接测试失败: %v", err)
}
return nil
}
// 获取WH65LP站点列表
func getWH65LPStations(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
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 := db.Query(query)
if err != nil {
http.Error(w, "查询站点失败", http.StatusInternalServerError)
log.Printf("查询站点失败: %v", err)
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 {
log.Printf("扫描站点数据失败: %v", err)
continue
}
station.LastUpdate = lastUpdate.Format("2006-01-02 15:04:05")
stations = append(stations, station)
}
json.NewEncoder(w).Encode(stations)
}
// 获取站点历史数据
func getStationData(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
stationID := r.URL.Query().Get("station_id")
startTime := r.URL.Query().Get("start_time")
endTime := r.URL.Query().Get("end_time")
interval := r.URL.Query().Get("interval")
if stationID == "" || startTime == "" || endTime == "" {
http.Error(w, "缺少必要参数", http.StatusBadRequest)
return
}
// 默认间隔为1小时
if interval == "" {
interval = "1hour"
}
var query string
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 := db.Query(query, stationID, startTime, endTime)
if err != nil {
http.Error(w, "查询数据失败", http.StatusInternalServerError)
log.Printf("查询数据失败: %v", err)
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 {
log.Printf("扫描数据失败: %v", err)
continue
}
point.DateTime = timestamp.Format("2006-01-02 15:04:05")
data = append(data, point)
}
json.NewEncoder(w).Encode(data)
}
// 提供静态文件服务
func serveStaticFiles() {
// 获取当前工作目录
workDir := "/home/yarnom/Archive/code/WeatherStation"
webDir := filepath.Join(workDir, "web")
// 创建文件服务器
fs := http.FileServer(http.Dir(webDir))
// 处理根路径请求
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
http.ServeFile(w, r, filepath.Join(webDir, "index.html"))
return
}
// 检查文件是否存在
if _, err := http.Dir(webDir).Open(strings.TrimPrefix(r.URL.Path, "/")); err != nil {
http.NotFound(w, r)
return
}
// 提供静态文件
fs.ServeHTTP(w, r)
})
}
func StartWebServer() {
err := initWebDB()
if err != nil {
log.Fatalf("初始化Web数据库连接失败: %v", err)
}
// API路由
http.HandleFunc("/api/stations", getWH65LPStations)
http.HandleFunc("/api/data", getStationData)
// 静态文件服务
serveStaticFiles()
log.Println("Web服务器启动监听端口 10003...")
log.Fatal(http.ListenAndServe(":10003", nil))
}