feat: 更新内部包
This commit is contained in:
parent
54fa9d6186
commit
34b7bf3ff2
@ -5,11 +5,13 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"weatherstation/core/internal/config"
|
"weatherstation/core/internal/config"
|
||||||
|
"weatherstation/core/internal/data"
|
||||||
"weatherstation/core/internal/server"
|
"weatherstation/core/internal/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
cfg := config.Load()
|
cfg := config.Load()
|
||||||
|
_ = data.DB()
|
||||||
r := server.NewRouter(server.Options{
|
r := server.NewRouter(server.Options{
|
||||||
UIServeDir: cfg.UIServeDir,
|
UIServeDir: cfg.UIServeDir,
|
||||||
TemplateDir: cfg.TemplateDir,
|
TemplateDir: cfg.TemplateDir,
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
@ -26,7 +25,6 @@ func main() {
|
|||||||
opts.SetKeepAlive(60 * time.Second)
|
opts.SetKeepAlive(60 * time.Second)
|
||||||
opts.SetAutoReconnect(true)
|
opts.SetAutoReconnect(true)
|
||||||
opts.SetCleanSession(true)
|
opts.SetCleanSession(true)
|
||||||
opts.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
|
|
||||||
|
|
||||||
c := mqtt.NewClient(opts)
|
c := mqtt.NewClient(opts)
|
||||||
if t := c.Connect(); t.Wait() && t.Error() != nil {
|
if t := c.Connect(); t.Wait() && t.Error() != nil {
|
||||||
|
|||||||
@ -2,7 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
@ -19,7 +18,7 @@ const (
|
|||||||
deviceID = "Z866"
|
deviceID = "Z866"
|
||||||
stationID = "RS485-002A6E"
|
stationID = "RS485-002A6E"
|
||||||
|
|
||||||
// MQTT 测试配置(可替换为生产)
|
// MQTT
|
||||||
brokerURL = "wss://broker.emqx.io:8084/mqtt"
|
brokerURL = "wss://broker.emqx.io:8084/mqtt"
|
||||||
clientID = "core-publisher-Z866"
|
clientID = "core-publisher-Z866"
|
||||||
username = "1"
|
username = "1"
|
||||||
@ -49,7 +48,6 @@ func main() {
|
|||||||
opts.SetKeepAlive(60 * time.Second)
|
opts.SetKeepAlive(60 * time.Second)
|
||||||
opts.SetAutoReconnect(true)
|
opts.SetAutoReconnect(true)
|
||||||
opts.SetCleanSession(true)
|
opts.SetCleanSession(true)
|
||||||
opts.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
|
|
||||||
|
|
||||||
cli := mqtt.NewClient(opts)
|
cli := mqtt.NewClient(opts)
|
||||||
if tok := cli.Connect(); tok.Wait() && tok.Error() != nil {
|
if tok := cli.Connect(); tok.Wait() && tok.Error() != nil {
|
||||||
|
|||||||
39
core/internal/data/db.go
Normal file
39
core/internal/data/db.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
legacycfg "weatherstation/internal/config"
|
||||||
|
|
||||||
|
_ "github.com/lib/pq"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
dbOnce sync.Once
|
||||||
|
dbInst *sql.DB
|
||||||
|
)
|
||||||
|
|
||||||
|
// DB returns a shared Postgres connection opened using legacy internal/config.
|
||||||
|
func DB() *sql.DB {
|
||||||
|
dbOnce.Do(func() {
|
||||||
|
cfg := legacycfg.GetConfig().Database
|
||||||
|
connStr := fmt.Sprintf(
|
||||||
|
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
|
||||||
|
cfg.Host, cfg.Port, cfg.User, cfg.Password, cfg.DBName, cfg.SSLMode,
|
||||||
|
)
|
||||||
|
d, err := sql.Open("postgres", connStr)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("open DB failed: %w", err))
|
||||||
|
}
|
||||||
|
if err := d.Ping(); err != nil {
|
||||||
|
panic(fmt.Errorf("ping DB failed: %w", err))
|
||||||
|
}
|
||||||
|
dbInst = d
|
||||||
|
})
|
||||||
|
if dbInst == nil {
|
||||||
|
panic("database not initialized: check config and drivers")
|
||||||
|
}
|
||||||
|
return dbInst
|
||||||
|
}
|
||||||
@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"time"
|
"time"
|
||||||
"weatherstation/internal/database"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type PredictPoint struct {
|
type PredictPoint struct {
|
||||||
@ -19,7 +18,7 @@ func ForecastRainAtIssued(ctx context.Context, stationID, provider string, issue
|
|||||||
FROM forecast_hourly
|
FROM forecast_hourly
|
||||||
WHERE station_id=$1 AND provider=$2 AND issued_at=$3
|
WHERE station_id=$1 AND provider=$2 AND issued_at=$3
|
||||||
ORDER BY forecast_time ASC`
|
ORDER BY forecast_time ASC`
|
||||||
rows, err := database.GetDB().QueryContext(ctx, q, stationID, provider, issuedAt)
|
rows, err := DB().QueryContext(ctx, q, stationID, provider, issuedAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"math"
|
"math"
|
||||||
"time"
|
"time"
|
||||||
"weatherstation/internal/database"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type WindowAgg struct {
|
type WindowAgg struct {
|
||||||
@ -34,7 +33,7 @@ func WindowAverages(ctx context.Context, stationID string, start, end time.Time)
|
|||||||
FROM rs485_weather_data
|
FROM rs485_weather_data
|
||||||
WHERE station_id = $1 AND timestamp >= $2 AND timestamp < $3`
|
WHERE station_id = $1 AND timestamp >= $2 AND timestamp < $3`
|
||||||
var agg WindowAgg
|
var agg WindowAgg
|
||||||
err := database.GetDB().QueryRowContext(ctx, q, stationID, start, end).Scan(
|
err := DB().QueryRowContext(ctx, q, stationID, start, end).Scan(
|
||||||
&agg.Ta, &agg.Ua, &agg.Pa, &agg.Sm, &agg.Dm,
|
&agg.Ta, &agg.Ua, &agg.Pa, &agg.Sm, &agg.Dm,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -68,11 +67,11 @@ func DailyRainSinceMidnight(ctx context.Context, stationID string, now time.Time
|
|||||||
|
|
||||||
var baseline sql.NullFloat64
|
var baseline sql.NullFloat64
|
||||||
const qBase = `SELECT rainfall FROM rs485_weather_data WHERE station_id=$1 AND timestamp <= $2 ORDER BY timestamp DESC LIMIT 1`
|
const qBase = `SELECT rainfall FROM rs485_weather_data WHERE station_id=$1 AND timestamp <= $2 ORDER BY timestamp DESC LIMIT 1`
|
||||||
_ = database.GetDB().QueryRowContext(ctx, qBase, stationID, dayStart).Scan(&baseline)
|
_ = DB().QueryRowContext(ctx, qBase, stationID, dayStart).Scan(&baseline)
|
||||||
|
|
||||||
var current sql.NullFloat64
|
var current sql.NullFloat64
|
||||||
const qCur = `SELECT rainfall FROM rs485_weather_data WHERE station_id=$1 ORDER BY timestamp DESC LIMIT 1`
|
const qCur = `SELECT rainfall FROM rs485_weather_data WHERE station_id=$1 ORDER BY timestamp DESC LIMIT 1`
|
||||||
_ = database.GetDB().QueryRowContext(ctx, qCur, stationID).Scan(¤t)
|
_ = DB().QueryRowContext(ctx, qCur, stationID).Scan(¤t)
|
||||||
|
|
||||||
if !current.Valid || !baseline.Valid {
|
if !current.Valid || !baseline.Valid {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
|
|||||||
@ -1,26 +1,283 @@
|
|||||||
package data
|
package data
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"fmt"
|
||||||
|
"log"
|
||||||
"time"
|
"time"
|
||||||
"weatherstation/internal/database"
|
|
||||||
"weatherstation/pkg/types"
|
"weatherstation/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
func DB() *sql.DB { return database.GetDB() }
|
func OnlineDevices() int {
|
||||||
|
const query = `SELECT COUNT(DISTINCT station_id) FROM rs485_weather_data WHERE timestamp > NOW() - INTERVAL '5 minutes'`
|
||||||
|
var count int
|
||||||
|
if err := DB().QueryRow(query).Scan(&count); err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
func OnlineDevices() int { return database.GetOnlineDevicesCount(DB()) }
|
func Stations() ([]types.Station, error) {
|
||||||
|
const query = `
|
||||||
func Stations() ([]types.Station, error) { return database.GetStations(DB()) }
|
SELECT DISTINCT s.station_id,
|
||||||
|
COALESCE(s.station_alias, '') as station_alias,
|
||||||
|
COALESCE(s.password, '') as station_name,
|
||||||
|
'WH65LP' as device_type,
|
||||||
|
COALESCE(MAX(r.timestamp), '1970-01-01'::timestamp) as last_update,
|
||||||
|
COALESCE(s.latitude, 0) as latitude,
|
||||||
|
COALESCE(s.longitude, 0) as longitude,
|
||||||
|
COALESCE(s.name, '') as name,
|
||||||
|
COALESCE(s.location, '') as location,
|
||||||
|
COALESCE(s.z, 0) as z,
|
||||||
|
COALESCE(s.y, 0) as y,
|
||||||
|
COALESCE(s.x, 0) as x
|
||||||
|
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.station_alias, s.password, s.latitude, s.longitude, s.name, s.location, s.z, s.y, s.x
|
||||||
|
ORDER BY s.station_id`
|
||||||
|
rows, err := DB().Query(query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var stations []types.Station
|
||||||
|
for rows.Next() {
|
||||||
|
var s types.Station
|
||||||
|
var last time.Time
|
||||||
|
if err := rows.Scan(&s.StationID, &s.StationAlias, &s.StationName, &s.DeviceType, &last, &s.Latitude, &s.Longitude, &s.Name, &s.Location, &s.Z, &s.Y, &s.X); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.LastUpdate = last.Format("2006-01-02 15:04:05")
|
||||||
|
stations = append(stations, s)
|
||||||
|
}
|
||||||
|
return stations, nil
|
||||||
|
}
|
||||||
|
|
||||||
func SeriesRaw(stationID string, start, end time.Time) ([]types.WeatherPoint, error) {
|
func SeriesRaw(stationID string, start, end time.Time) ([]types.WeatherPoint, error) {
|
||||||
return database.GetSeriesRaw(DB(), stationID, start, end)
|
const query = `
|
||||||
|
SELECT
|
||||||
|
to_char(timestamp, 'YYYY-MM-DD HH24:MI:SS') AS date_time,
|
||||||
|
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, 0) AS rainfall,
|
||||||
|
COALESCE(light, 0) AS light,
|
||||||
|
COALESCE(uv, 0) AS uv,
|
||||||
|
COALESCE(rainfall, 0) AS rain_total
|
||||||
|
FROM rs485_weather_data
|
||||||
|
WHERE station_id = $1 AND timestamp >= $2 AND timestamp <= $3
|
||||||
|
ORDER BY timestamp`
|
||||||
|
rows, err := DB().Query(query, stationID, start, end)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var points []types.WeatherPoint
|
||||||
|
for rows.Next() {
|
||||||
|
var p types.WeatherPoint
|
||||||
|
if err := rows.Scan(&p.DateTime, &p.Temperature, &p.Humidity, &p.Pressure, &p.WindSpeed, &p.WindDir, &p.Rainfall, &p.Light, &p.UV, &p.RainTotal); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
points = append(points, p)
|
||||||
|
}
|
||||||
|
return points, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func SeriesFrom10Min(stationID string, start, end time.Time, interval string) ([]types.WeatherPoint, error) {
|
func SeriesFrom10Min(stationID string, start, end time.Time, interval string) ([]types.WeatherPoint, error) {
|
||||||
return database.GetSeriesFrom10Min(DB(), stationID, start, end, interval)
|
log.Printf("查询数据: stationID=%s, start=%v, end=%v, interval=%s", stationID, start.Format("2006-01-02 15:04:05"), end.Format("2006-01-02 15:04:05"), interval)
|
||||||
|
var query string
|
||||||
|
switch interval {
|
||||||
|
case "10min":
|
||||||
|
query = `
|
||||||
|
SELECT
|
||||||
|
to_char(bucket_start + interval '10 minutes', 'YYYY-MM-DD HH24:MI:SS') AS date_time,
|
||||||
|
ROUND(temp_c_x100/100.0, 2) AS temperature,
|
||||||
|
ROUND(humidity_pct::numeric, 2) AS humidity,
|
||||||
|
ROUND(pressure_hpa_x100/100.0, 2) AS pressure,
|
||||||
|
ROUND(wind_speed_ms_x1000/1000.0, 3) AS wind_speed,
|
||||||
|
ROUND(wind_dir_deg::numeric, 2) AS wind_direction,
|
||||||
|
ROUND(rain_10m_mm_x1000/1000.0, 3) AS rainfall,
|
||||||
|
ROUND(solar_wm2_x100/100.0, 2) AS light,
|
||||||
|
ROUND(uv_index::numeric, 2) AS uv,
|
||||||
|
ROUND(rain_total_mm_x1000/1000.0, 3) AS rain_total
|
||||||
|
FROM rs485_weather_10min
|
||||||
|
WHERE station_id = $1 AND bucket_start >= $2 AND bucket_start <= $3
|
||||||
|
ORDER BY bucket_start + interval '10 minutes'`
|
||||||
|
case "30min":
|
||||||
|
query = buildAggFrom10MinQuery("30 minutes")
|
||||||
|
default:
|
||||||
|
query = buildAggFrom10MinQuery("1 hour")
|
||||||
|
}
|
||||||
|
rows, err := DB().Query(query, stationID, start, end)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("查询失败: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var points []types.WeatherPoint
|
||||||
|
for rows.Next() {
|
||||||
|
var p types.WeatherPoint
|
||||||
|
if err := rows.Scan(&p.DateTime, &p.Temperature, &p.Humidity, &p.Pressure, &p.WindSpeed, &p.WindDir, &p.Rainfall, &p.Light, &p.UV, &p.RainTotal); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
points = append(points, p)
|
||||||
|
}
|
||||||
|
return points, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildAggFrom10MinQuery(interval string) string {
|
||||||
|
return `
|
||||||
|
WITH base AS (
|
||||||
|
SELECT * FROM rs485_weather_10min
|
||||||
|
WHERE station_id = $1 AND bucket_start >= $2 AND bucket_start <= $3
|
||||||
|
), g AS (
|
||||||
|
SELECT
|
||||||
|
CASE '` + interval + `'
|
||||||
|
WHEN '1 hour' THEN date_trunc('hour', bucket_start)
|
||||||
|
WHEN '30 minutes' THEN
|
||||||
|
date_trunc('hour', bucket_start) +
|
||||||
|
CASE WHEN date_part('minute', bucket_start) >= 30
|
||||||
|
THEN '30 minutes'::interval
|
||||||
|
ELSE '0 minutes'::interval
|
||||||
|
END
|
||||||
|
END AS grp,
|
||||||
|
SUM(temp_c_x100 * sample_count)::bigint AS w_temp,
|
||||||
|
SUM(humidity_pct * sample_count)::bigint AS w_hum,
|
||||||
|
SUM(pressure_hpa_x100 * sample_count)::bigint AS w_p,
|
||||||
|
SUM(solar_wm2_x100 * sample_count)::bigint AS w_solar,
|
||||||
|
SUM(uv_index * sample_count)::bigint AS w_uv,
|
||||||
|
SUM(wind_speed_ms_x1000 * sample_count)::bigint AS w_ws,
|
||||||
|
MAX(wind_gust_ms_x1000) AS gust_max,
|
||||||
|
SUM(sin(radians(wind_dir_deg)) * sample_count)::double precision AS sin_sum,
|
||||||
|
SUM(cos(radians(wind_dir_deg)) * sample_count)::double precision AS cos_sum,
|
||||||
|
SUM(rain_10m_mm_x1000) AS rain_sum,
|
||||||
|
SUM(sample_count) AS n_sum,
|
||||||
|
MAX(rain_total_mm_x1000) AS rain_total_max
|
||||||
|
FROM base
|
||||||
|
GROUP BY 1
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
to_char(grp + '` + interval + `'::interval, 'YYYY-MM-DD HH24:MI:SS') AS date_time,
|
||||||
|
ROUND((w_temp/NULLIF(n_sum,0))/100.0, 2) AS temperature,
|
||||||
|
ROUND((w_hum/NULLIF(n_sum,0))::numeric, 2) AS humidity,
|
||||||
|
ROUND((w_p/NULLIF(n_sum,0))/100.0, 2) AS pressure,
|
||||||
|
ROUND((w_ws/NULLIF(n_sum,0))/1000.0, 3) AS wind_speed,
|
||||||
|
ROUND((CASE WHEN degrees(atan2(sin_sum, cos_sum)) < 0
|
||||||
|
THEN degrees(atan2(sin_sum, cos_sum)) + 360
|
||||||
|
ELSE degrees(atan2(sin_sum, cos_sum)) END)::numeric, 2) AS wind_direction,
|
||||||
|
ROUND((rain_sum/1000.0)::numeric, 3) AS rainfall,
|
||||||
|
ROUND((w_solar/NULLIF(n_sum,0))/100.0, 2) AS light,
|
||||||
|
ROUND((w_uv/NULLIF(n_sum,0))::numeric, 2) AS uv,
|
||||||
|
ROUND((rain_total_max/1000.0)::numeric, 3) AS rain_total
|
||||||
|
FROM g
|
||||||
|
ORDER BY grp + '` + interval + `'::interval`
|
||||||
}
|
}
|
||||||
|
|
||||||
func Forecast(stationID string, start, end time.Time, provider string, versions int) ([]types.ForecastPoint, error) {
|
func Forecast(stationID string, start, end time.Time, provider string, versions int) ([]types.ForecastPoint, error) {
|
||||||
return database.GetForecastData(DB(), stationID, start, end, provider, versions)
|
var query string
|
||||||
|
var args []interface{}
|
||||||
|
if versions <= 0 {
|
||||||
|
versions = 1
|
||||||
|
}
|
||||||
|
if provider != "" {
|
||||||
|
if provider == "open-meteo" {
|
||||||
|
query = `
|
||||||
|
WITH ranked AS (
|
||||||
|
SELECT station_id, provider, issued_at, forecast_time,
|
||||||
|
temp_c_x100, humidity_pct, wind_speed_ms_x1000, wind_gust_ms_x1000,
|
||||||
|
wind_dir_deg, rain_mm_x1000, precip_prob_pct, uv_index, pressure_hpa_x100,
|
||||||
|
ROW_NUMBER() OVER (PARTITION BY forecast_time ORDER BY issued_at DESC) AS rn,
|
||||||
|
CEIL(EXTRACT(EPOCH FROM (forecast_time - issued_at)) / 3600.0)::int AS lead_hours
|
||||||
|
FROM forecast_hourly
|
||||||
|
WHERE station_id = $1 AND provider IN ('open-meteo','open-meteo_historical')
|
||||||
|
AND forecast_time BETWEEN $2 AND $3
|
||||||
|
)
|
||||||
|
SELECT to_char(forecast_time, 'YYYY-MM-DD HH24:MI:SS') as date_time,
|
||||||
|
provider,
|
||||||
|
to_char(issued_at, 'YYYY-MM-DD HH24:MI:SS') as issued_at,
|
||||||
|
ROUND(temp_c_x100::numeric / 100.0, 2) as temperature,
|
||||||
|
humidity_pct as humidity,
|
||||||
|
ROUND(pressure_hpa_x100::numeric / 100.0, 2) as pressure,
|
||||||
|
ROUND(wind_speed_ms_x1000::numeric / 1000.0, 2) as wind_speed,
|
||||||
|
wind_dir_deg as wind_direction,
|
||||||
|
ROUND(rain_mm_x1000::numeric / 1000.0, 3) as rainfall,
|
||||||
|
precip_prob_pct as precip_prob,
|
||||||
|
uv_index as uv,
|
||||||
|
lead_hours
|
||||||
|
FROM ranked WHERE rn <= $4
|
||||||
|
ORDER BY forecast_time, issued_at DESC`
|
||||||
|
args = []interface{}{stationID, start.Format("2006-01-02 15:04:05-07"), end.Format("2006-01-02 15:04:05-07"), versions}
|
||||||
|
} else {
|
||||||
|
query = `
|
||||||
|
WITH ranked AS (
|
||||||
|
SELECT station_id, provider, issued_at, forecast_time,
|
||||||
|
temp_c_x100, humidity_pct, wind_speed_ms_x1000, wind_gust_ms_x1000,
|
||||||
|
wind_dir_deg, rain_mm_x1000, precip_prob_pct, uv_index, pressure_hpa_x100,
|
||||||
|
ROW_NUMBER() OVER (PARTITION BY forecast_time ORDER BY issued_at DESC) AS rn,
|
||||||
|
CEIL(EXTRACT(EPOCH FROM (forecast_time - issued_at)) / 3600.0)::int AS lead_hours
|
||||||
|
FROM forecast_hourly
|
||||||
|
WHERE station_id = $1 AND provider = $2
|
||||||
|
AND forecast_time BETWEEN $3 AND $4
|
||||||
|
)
|
||||||
|
SELECT to_char(forecast_time, 'YYYY-MM-DD HH24:MI:SS') as date_time,
|
||||||
|
provider,
|
||||||
|
to_char(issued_at, 'YYYY-MM-DD HH24:MI:SS') as issued_at,
|
||||||
|
ROUND(temp_c_x100::numeric / 100.0, 2) as temperature,
|
||||||
|
humidity_pct as humidity,
|
||||||
|
ROUND(pressure_hpa_x100::numeric / 100.0, 2) as pressure,
|
||||||
|
ROUND(wind_speed_ms_x1000::numeric / 1000.0, 2) as wind_speed,
|
||||||
|
wind_dir_deg as wind_direction,
|
||||||
|
ROUND(rain_mm_x1000::numeric / 1000.0, 3) as rainfall,
|
||||||
|
precip_prob_pct as precip_prob,
|
||||||
|
uv_index as uv,
|
||||||
|
lead_hours
|
||||||
|
FROM ranked WHERE rn <= $5
|
||||||
|
ORDER BY forecast_time, issued_at DESC`
|
||||||
|
args = []interface{}{stationID, provider, start.Format("2006-01-02 15:04:05-07"), end.Format("2006-01-02 15:04:05-07"), versions}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
query = `
|
||||||
|
WITH ranked AS (
|
||||||
|
SELECT station_id, provider, issued_at, forecast_time,
|
||||||
|
temp_c_x100, humidity_pct, wind_speed_ms_x1000, wind_gust_ms_x1000,
|
||||||
|
wind_dir_deg, rain_mm_x1000, precip_prob_pct, uv_index, pressure_hpa_x100,
|
||||||
|
ROW_NUMBER() OVER (PARTITION BY provider, forecast_time ORDER BY issued_at DESC) AS rn,
|
||||||
|
CEIL(EXTRACT(EPOCH FROM (forecast_time - issued_at)) / 3600.0)::int AS lead_hours
|
||||||
|
FROM forecast_hourly
|
||||||
|
WHERE station_id = $1 AND forecast_time BETWEEN $2 AND $3
|
||||||
|
)
|
||||||
|
SELECT to_char(forecast_time, 'YYYY-MM-DD HH24:MI:SS') as date_time,
|
||||||
|
provider,
|
||||||
|
to_char(issued_at, 'YYYY-MM-DD HH24:MI:SS') as issued_at,
|
||||||
|
ROUND(temp_c_x100::numeric / 100.0, 2) as temperature,
|
||||||
|
humidity_pct as humidity,
|
||||||
|
ROUND(pressure_hpa_x100::numeric / 100.0, 2) as pressure,
|
||||||
|
ROUND(wind_speed_ms_x1000::numeric / 1000.0, 2) as wind_speed,
|
||||||
|
wind_dir_deg as wind_direction,
|
||||||
|
ROUND(rain_mm_x1000::numeric / 1000.0, 3) as rainfall,
|
||||||
|
precip_prob_pct as precip_prob,
|
||||||
|
uv_index as uv,
|
||||||
|
lead_hours
|
||||||
|
FROM ranked WHERE rn <= $4
|
||||||
|
ORDER BY forecast_time, provider, issued_at DESC`
|
||||||
|
args = []interface{}{stationID, start.Format("2006-01-02 15:04:05-07"), end.Format("2006-01-02 15:04:05-07"), versions}
|
||||||
|
}
|
||||||
|
rows, err := DB().Query(query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("查询预报数据失败: %v", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var points []types.ForecastPoint
|
||||||
|
for rows.Next() {
|
||||||
|
var p types.ForecastPoint
|
||||||
|
if err := rows.Scan(&p.DateTime, &p.Provider, &p.IssuedAt, &p.Temperature, &p.Humidity, &p.Pressure, &p.WindSpeed, &p.WindDir, &p.Rainfall, &p.PrecipProb, &p.UV, &p.LeadHours); err != nil {
|
||||||
|
log.Printf("数据扫描错误: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p.Source = "forecast"
|
||||||
|
points = append(points, p)
|
||||||
|
}
|
||||||
|
return points, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
"weatherstation/internal/database"
|
"weatherstation/core/internal/data"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@ -61,11 +61,11 @@ func handleRadarTimes(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const qRange = `SELECT dt FROM radar_tiles WHERE z=$1 AND y=$2 AND x=$3 AND dt BETWEEN $4 AND $5 ORDER BY dt DESC`
|
const qRange = `SELECT dt FROM radar_tiles WHERE z=$1 AND y=$2 AND x=$3 AND dt BETWEEN $4 AND $5 ORDER BY dt DESC`
|
||||||
rows, err = database.GetDB().Query(qRange, z, y, x, from, to)
|
rows, err = data.DB().Query(qRange, z, y, x, from, to)
|
||||||
} else {
|
} else {
|
||||||
limit := parseInt(c.Query("limit"), 48)
|
limit := parseInt(c.Query("limit"), 48)
|
||||||
const q = `SELECT dt FROM radar_tiles WHERE z=$1 AND y=$2 AND x=$3 ORDER BY dt DESC LIMIT $4`
|
const q = `SELECT dt FROM radar_tiles WHERE z=$1 AND y=$2 AND x=$3 ORDER BY dt DESC LIMIT $4`
|
||||||
rows, err = database.GetDB().Query(q, z, y, x, limit)
|
rows, err = data.DB().Query(q, z, y, x, limit)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "query failed"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "query failed"})
|
||||||
@ -100,7 +100,7 @@ func handleRadarTilesAt(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const q = `SELECT dt,z,y,x,width,height,west,south,east,north,res_deg,data FROM radar_tiles WHERE z=$1 AND dt=$2 ORDER BY y,x`
|
const q = `SELECT dt,z,y,x,width,height,west,south,east,north,res_deg,data FROM radar_tiles WHERE z=$1 AND dt=$2 ORDER BY y,x`
|
||||||
rows, qerr := database.GetDB().Query(q, z, dt)
|
rows, qerr := data.DB().Query(q, z, dt)
|
||||||
if qerr != nil {
|
if qerr != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db failed"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "db failed"})
|
||||||
return
|
return
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
"weatherstation/internal/database"
|
"weatherstation/core/internal/data"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@ -61,11 +61,11 @@ func handleRainTimes(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const qRange = `SELECT dt FROM rain_tiles WHERE z=$1 AND y=$2 AND x=$3 AND dt BETWEEN $4 AND $5 ORDER BY dt DESC`
|
const qRange = `SELECT dt FROM rain_tiles WHERE z=$1 AND y=$2 AND x=$3 AND dt BETWEEN $4 AND $5 ORDER BY dt DESC`
|
||||||
rows, err = database.GetDB().Query(qRange, z, y, x, from, to)
|
rows, err = data.DB().Query(qRange, z, y, x, from, to)
|
||||||
} else {
|
} else {
|
||||||
limit := parseInt(c.Query("limit"), 48)
|
limit := parseInt(c.Query("limit"), 48)
|
||||||
const q = `SELECT dt FROM rain_tiles WHERE z=$1 AND y=$2 AND x=$3 ORDER BY dt DESC LIMIT $4`
|
const q = `SELECT dt FROM rain_tiles WHERE z=$1 AND y=$2 AND x=$3 ORDER BY dt DESC LIMIT $4`
|
||||||
rows, err = database.GetDB().Query(q, z, y, x, limit)
|
rows, err = data.DB().Query(q, z, y, x, limit)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "query failed"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "query failed"})
|
||||||
@ -100,7 +100,7 @@ func handleRainTilesAt(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const q = `SELECT dt,z,y,x,width,height,west,south,east,north,res_deg,data FROM rain_tiles WHERE z=$1 AND dt=$2 ORDER BY y,x`
|
const q = `SELECT dt,z,y,x,width,height,west,south,east,north,res_deg,data FROM rain_tiles WHERE z=$1 AND dt=$2 ORDER BY y,x`
|
||||||
rows, qerr := database.GetDB().Query(q, z, dt)
|
rows, qerr := data.DB().Query(q, z, dt)
|
||||||
if qerr != nil {
|
if qerr != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db failed"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "db failed"})
|
||||||
return
|
return
|
||||||
|
|||||||
3
go.mod
3
go.mod
@ -5,6 +5,7 @@ go 1.23.0
|
|||||||
toolchain go1.24.5
|
toolchain go1.24.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/eclipse/paho.mqtt.golang v1.4.3
|
||||||
github.com/gin-gonic/gin v1.10.1
|
github.com/gin-gonic/gin v1.10.1
|
||||||
github.com/go-sql-driver/mysql v1.8.1
|
github.com/go-sql-driver/mysql v1.8.1
|
||||||
github.com/lib/pq v1.10.9
|
github.com/lib/pq v1.10.9
|
||||||
@ -23,6 +24,7 @@ require (
|
|||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
|
github.com/gorilla/websocket v1.5.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
@ -35,6 +37,7 @@ require (
|
|||||||
golang.org/x/arch v0.8.0 // indirect
|
golang.org/x/arch v0.8.0 // indirect
|
||||||
golang.org/x/crypto v0.23.0 // indirect
|
golang.org/x/crypto v0.23.0 // indirect
|
||||||
golang.org/x/net v0.25.0 // indirect
|
golang.org/x/net v0.25.0 // indirect
|
||||||
|
golang.org/x/sync v0.1.0 // indirect
|
||||||
golang.org/x/sys v0.20.0 // indirect
|
golang.org/x/sys v0.20.0 // indirect
|
||||||
golang.org/x/text v0.15.0 // indirect
|
golang.org/x/text v0.15.0 // indirect
|
||||||
google.golang.org/protobuf v1.34.1 // indirect
|
google.golang.org/protobuf v1.34.1 // indirect
|
||||||
|
|||||||
6
go.sum
6
go.sum
@ -11,6 +11,8 @@ github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQ
|
|||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/eclipse/paho.mqtt.golang v1.4.3 h1:2kwcUGn8seMUfWndX0hGbvH8r7crgcJguQNCyp70xik=
|
||||||
|
github.com/eclipse/paho.mqtt.golang v1.4.3/go.mod h1:CSYvoAlsMkhYOXh/oKyxa8EcBci6dVkLCbo5tTC1RIE=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
@ -32,6 +34,8 @@ github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG
|
|||||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||||
|
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
@ -76,6 +80,8 @@ golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
|||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
|
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user