241 lines
6.1 KiB
Go
241 lines
6.1 KiB
Go
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 '1 hour' +
|
||
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(×tamp, &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))
|
||
}
|