2025-08-24 15:18:03 +08:00

214 lines
6.9 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 forecast
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"io"
"log"
"math"
"net/http"
"time"
"weatherstation/internal/database"
)
// 彩云返回结构(仅取用需要的字段)
type caiyunHourly struct {
Status string `json:"status"`
Result struct {
Hourly struct {
Status string `json:"status"`
Temperature []struct {
Datetime string `json:"datetime"`
Value float64 `json:"value"`
} `json:"temperature"`
Humidity []struct {
Datetime string `json:"datetime"`
Value float64 `json:"value"`
} `json:"humidity"`
Wind []struct {
Datetime string `json:"datetime"`
Speed float64 `json:"speed"`
Direction float64 `json:"direction"`
} `json:"wind"`
Precipitation []struct {
Datetime string `json:"datetime"`
Value float64 `json:"value"`
Probability float64 `json:"probability"`
} `json:"precipitation"`
Pressure []struct {
Datetime string `json:"datetime"`
Value float64 `json:"value"`
} `json:"pressure"`
} `json:"hourly"`
} `json:"result"`
}
// RunCaiyunFetch 拉取各站点未来三小时并写入 forecast_hourlyprovider=caiyun
func RunCaiyunFetch(ctx context.Context, token string) error {
log.Printf("彩云抓取开始token=%s", token)
db := database.GetDB()
stations, err := loadStationsWithLatLon(ctx, db)
if err != nil {
log.Printf("加载站点失败: %v", err)
return err
}
log.Printf("找到 %d 个有经纬度的站点", len(stations))
client := &http.Client{Timeout: 15 * time.Second}
loc, _ := time.LoadLocation("Asia/Shanghai")
if loc == nil {
loc = time.FixedZone("CST", 8*3600)
}
issuedAt := time.Now().In(loc)
startHour := issuedAt.Truncate(time.Hour)
targets := []time.Time{startHour.Add(1 * time.Hour), startHour.Add(2 * time.Hour), startHour.Add(3 * time.Hour)}
for _, s := range stations {
if !s.lat.Valid || !s.lon.Valid {
continue
}
url := fmt.Sprintf("https://api.caiyunapp.com/v2.6/%s/%f,%f/hourly?hourlysteps=4&unit=metric:v2", token, s.lon.Float64, s.lat.Float64)
log.Printf("请求彩云 API: %s", url)
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
resp, err := client.Do(req)
if err != nil {
log.Printf("caiyun 请求失败 station=%s err=%v", s.id, err)
continue
}
log.Printf("彩云响应状态码: %d", resp.StatusCode)
var data caiyunHourly
body, _ := io.ReadAll(resp.Body)
log.Printf("彩云响应内容: %s", string(body))
resp.Body.Close()
if err := json.Unmarshal(body, &data); err != nil {
log.Printf("caiyun 解码失败 station=%s err=%v", s.id, err)
continue
}
log.Printf("彩云响应解析: status=%s", data.Status)
// 彩云时间戳形式例如 2022-05-26T16:00+08:00需按CST解析
// 建立 time->vals 映射
table := map[time.Time]struct {
rain float64
temp float64
rh float64
ws float64
wdir float64
prob float64
pres float64
}{}
// 温度 ℃
for _, t := range data.Result.Hourly.Temperature {
log.Printf("解析时间: %s", t.Datetime)
if ft, err := time.ParseInLocation("2006-01-02T15:04-07:00", t.Datetime, loc); err == nil {
log.Printf("解析结果: %v", ft)
v := table[ft]
v.temp = t.Value
table[ft] = v
} else {
log.Printf("时间解析失败: %v", err)
}
}
// 湿度 比例(0..1) 转换为 %
for _, h := range data.Result.Hourly.Humidity {
if ft, err := time.ParseInLocation("2006-01-02T15:04-07:00", h.Datetime, loc); err == nil {
v := table[ft]
v.rh = h.Value * 100.0
table[ft] = v
}
}
// 风metric:v2速度为km/h这里转换为m/s方向为度
for _, w := range data.Result.Hourly.Wind {
if ft, err := time.ParseInLocation("2006-01-02T15:04-07:00", w.Datetime, loc); err == nil {
v := table[ft]
v.ws = w.Speed / 3.6
v.wdir = w.Direction
table[ft] = v
}
}
// 降水 该小时量 mm概率 0..1 → %
for _, p := range data.Result.Hourly.Precipitation {
if ft, err := time.ParseInLocation("2006-01-02T15:04-07:00", p.Datetime, loc); err == nil {
v := table[ft]
v.rain = p.Value
// 直接使用API返回的概率值只进行范围限制
prob := p.Probability
// 四舍五入并确保在0-100范围内
prob = math.Round(prob)
if prob < 0 {
prob = 0
}
if prob > 100 {
prob = 100
}
v.prob = prob
table[ft] = v
}
}
// 气压:单位为 Pa转换为 hPaPa/100
for _, pr := range data.Result.Hourly.Pressure {
if ft, err := time.ParseInLocation("2006-01-02T15:04-07:00", pr.Datetime, loc); err == nil {
v := table[ft]
v.pres = pr.Value / 100.0
table[ft] = v
}
}
log.Printf("处理时间点: %v", targets)
for _, ft := range targets {
if v, ok := table[ft]; ok {
log.Printf("写入预报点: station=%s time=%s rain=%.3f temp=%.2f rh=%.1f ws=%.3f wdir=%.1f prob=%.1f pres=%.2f",
s.id, ft.Format(time.RFC3339), v.rain, v.temp, v.rh, v.ws, v.wdir, v.prob, v.pres)
if err := upsertForecastCaiyun(ctx, db, s.id, issuedAt, ft,
int64(v.rain*1000.0), // mm → x1000
int64(v.temp*100.0), // °C → x100
int64(v.rh), // %
int64(v.ws*1000.0), // m/s → x1000
int64(0), // gust: 彩云小时接口无阵风置0
int64(v.wdir), // 度
int64(v.prob), // %
int64(v.pres*100.0), // hPa → x100
); err != nil {
log.Printf("写入forecast失败(caiyun) station=%s time=%s err=%v", s.id, ft.Format(time.RFC3339), err)
} else {
log.Printf("写入forecast成功(caiyun) station=%s time=%s", s.id, ft.Format(time.RFC3339))
}
} else {
log.Printf("时间点无数据: %s", ft.Format(time.RFC3339))
}
}
}
return nil
}
func upsertForecastCaiyun(ctx context.Context, db *sql.DB, stationID string, issuedAt, forecastTime time.Time,
rainMmX1000, tempCx100, humidityPct, wsMsX1000, gustMsX1000, wdirDeg, probPct, pressureHpaX100 int64,
) error {
_, err := db.ExecContext(ctx, `
INSERT INTO forecast_hourly (
station_id, provider, issued_at, forecast_time,
rain_mm_x1000, temp_c_x100, humidity_pct, wind_speed_ms_x1000,
wind_gust_ms_x1000, wind_dir_deg, precip_prob_pct, pressure_hpa_x100
) VALUES ($1, 'caiyun', $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
ON CONFLICT (station_id, provider, issued_at, forecast_time)
DO UPDATE SET
rain_mm_x1000 = EXCLUDED.rain_mm_x1000,
temp_c_x100 = EXCLUDED.temp_c_x100,
humidity_pct = EXCLUDED.humidity_pct,
wind_speed_ms_x1000 = EXCLUDED.wind_speed_ms_x1000,
wind_gust_ms_x1000 = EXCLUDED.wind_gust_ms_x1000,
wind_dir_deg = EXCLUDED.wind_dir_deg,
precip_prob_pct = EXCLUDED.precip_prob_pct,
pressure_hpa_x100 = EXCLUDED.pressure_hpa_x100
`, stationID, issuedAt, forecastTime,
rainMmX1000, tempCx100, humidityPct, wsMsX1000, gustMsX1000, wdirDeg, probPct, pressureHpaX100)
return err
}