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(×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)) }