package service import ( "context" "encoding/json" "fmt" "io" "net/http" "net/url" "strconv" "time" ) const qweatherAPIBase = "https://devapi.qweather.com" // WeatherNow 实时天气 type WeatherNow struct { Temp float64 `json:"temp"` Humidity float64 `json:"humidity"` Text string `json:"text"` } // DailyForecast 逐日预报 type DailyForecast struct { Date string `json:"date"` TextDay string `json:"textDay"` TextNight string `json:"textNight"` TempMax float64 `json:"tempMax"` TempMin float64 `json:"tempMin"` Humidity float64 `json:"humidity"` } // WeatherService 和风天气客户端(未配置时 Configured()=false,调用返回明确错误) type WeatherService struct { key string location string baseURL string client *http.Client } // NewWeatherService 创建和风天气客户端 func NewWeatherService(key, location string) *WeatherService { return &WeatherService{ key: key, location: location, baseURL: qweatherAPIBase, client: &http.Client{Timeout: 15 * time.Second}, } } // Configured 是否已配置 API Key 与位置 func (s *WeatherService) Configured() bool { return s.key != "" && s.location != "" } // FetchNow 实时天气 func (s *WeatherService) FetchNow(ctx context.Context) (*WeatherNow, error) { var out struct { Code string `json:"code"` Now struct { Temp string `json:"temp"` Text string `json:"text"` Humidity string `json:"humidity"` } `json:"now"` } if err := s.get(ctx, "/v7/weather/now", &out); err != nil { return nil, err } temp, _ := strconv.ParseFloat(out.Now.Temp, 64) hum, _ := strconv.ParseFloat(out.Now.Humidity, 64) return &WeatherNow{Temp: temp, Humidity: hum, Text: out.Now.Text}, nil } // FetchDaily 未来 3 天预报 func (s *WeatherService) FetchDaily(ctx context.Context) ([]DailyForecast, error) { var out struct { Code string `json:"code"` Daily []struct { FxDate string `json:"fxDate"` TextDay string `json:"textDay"` TextNight string `json:"textNight"` TempMax string `json:"tempMax"` TempMin string `json:"tempMin"` Humidity string `json:"humidity"` } `json:"daily"` } if err := s.get(ctx, "/v7/weather/3d", &out); err != nil { return nil, err } list := make([]DailyForecast, 0, len(out.Daily)) for _, d := range out.Daily { max, _ := strconv.ParseFloat(d.TempMax, 64) min, _ := strconv.ParseFloat(d.TempMin, 64) hum, _ := strconv.ParseFloat(d.Humidity, 64) list = append(list, DailyForecast{ Date: d.FxDate, TextDay: d.TextDay, TextNight: d.TextNight, TempMax: max, TempMin: min, Humidity: hum, }) } return list, nil } // get 发起 GET 并校验 code=200 func (s *WeatherService) get(ctx context.Context, path string, out any) error { if !s.Configured() { return fmt.Errorf("天气服务未配置(QWEATHER_API_KEY/QWEATHER_LOCATION)") } u := s.baseURL + path + "?location=" + url.QueryEscape(s.location) + "&key=" + url.QueryEscape(s.key) req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil) if err != nil { return err } resp, err := s.client.Do(req) if err != nil { return err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return err } if err := json.Unmarshal(body, out); err != nil { return err } var head struct { Code string `json:"code"` } _ = json.Unmarshal(body, &head) if head.Code != "200" { return fmt.Errorf("和风天气返回错误 code=%s", head.Code) } return nil }