package main import ( "encoding/json" "fmt" "io" "log" "net/http" "regexp" "strings" "sync" "time" ) const qweatherKey = "3b67b65a53c04170b602d2d1a7e6096f" const qweatherHost = "https://pb4nmv4qnu.re.qweatherapi.com" type City struct { ID string `json:"id"` Name string `json:"name"` Adm2 string `json:"adm2"` Adm1 string `json:"adm1"` } var cities = []City{ {"101010100", "北京", "北京", "北京"}, {"101020100", "上海", "上海", "上海"}, {"101280101", "广州", "广州", "广东"}, {"101280601", "深圳", "深圳", "广东"}, {"101200101", "武汉", "武汉", "湖北"}, {"101110101", "西安", "西安", "陕西"}, {"101210101", "杭州", "杭州", "浙江"}, {"101190101", "南京", "南京", "江苏"}, {"101070101", "沈阳", "沈阳", "辽宁"}, {"101050101", "哈尔滨", "哈尔滨", "黑龙江"}, {"101250101", "长沙", "长沙", "湖南"}, {"101270101", "成都", "成都", "四川"}, {"101090101", "石家庄", "石家庄", "河北"}, {"101090206", "任丘", "任丘", "河北"}, {"101090301", "邯郸", "邯郸", "河北"}, {"101290106", "宣威", "宣威", "云南"}, {"101290101", "昆明", "昆明", "云南"}, {"101260101", "贵阳", "贵阳", "贵州"}, } var defaultCity = City{"101200101", "武汉", "武汉", "湖北"} var httpClient = &http.Client{Timeout: 10 * time.Second} var weatherIcons = map[string]string{ "晴": "☀️", "多云": "⛅", "阴": "☁️", "雨": "🌧️", "雪": "❄️", "雷": "⛈️", "雾": "🌫️", "霾": "😷", "风": "💨", } func getWeatherIcon(text string) string { for k, v := range weatherIcons { if strings.Contains(text, k) { return v } } return "🌤️" } func httpGet(url string) ([]byte, error) { resp, err := httpClient.Get(url) if err != nil { return nil, err } defer resp.Body.Close() return io.ReadAll(resp.Body) } // ── 定位 ── func getLocation() City { if city := locateByIPIP(); city != nil { return *city } if city := locateByGeoAPI(); city != nil { return *city } log.Println("所有定位失败,使用默认城市:", defaultCity.Name) return defaultCity } func locateByIPIP() *City { data, err := httpGet("https://myip.ipip.net") if err != nil { return nil } text := string(data) re := regexp.MustCompile(`来自于[::]\s*(.+?)\s*$`) m := re.FindStringSubmatch(text) if m == nil { return nil } parts := strings.Fields(m[1]) cityName := parts[0] if len(parts) >= 3 { cityName = parts[2] } else if len(parts) >= 2 { cityName = parts[1] } for _, c := range cities { if c.Name == cityName { log.Println("ipip.net 匹配:", c.Name) return &c } } return nil } func locateByGeoAPI() *City { data, err := httpGet("https://myip.ipip.net") if err == nil { re := regexp.MustCompile(`(\d+\.\d+\.\d+\.\d+)`) if m := re.FindStringSubmatch(string(data)); m != nil { return geoLookup(m[1]) } } return nil } func geoLookup(ip string) *City { url := fmt.Sprintf(qweatherHost+"/v2/city/lookup?location=%s&key=%s", ip, qweatherKey) data, err := httpGet(url) if err != nil { return nil } var resp struct { Code string `json:"code"` Location []struct { ID string `json:"id"` Name string `json:"name"` Adm2 string `json:"adm2"` Adm1 string `json:"adm1"` } `json:"location"` } if json.Unmarshal(data, &resp) != nil || resp.Code != "200" || len(resp.Location) == 0 { return nil } loc := resp.Location[0] for _, c := range cities { if c.Name == loc.Name { return &c } } return &City{loc.ID, loc.Name, loc.Adm2, loc.Adm1} } func getCurrentCity() City { cfg := loadConfig() if cfg.City != "" { for _, c := range cities { if c.ID == cfg.City { return c } } } return getLocation() } // ── 天气数据 ── type hourlyItem struct { Time string `json:"time"` Temp string `json:"temp"` Icon string `json:"icon"` } type dailyItem struct { Date string `json:"date"` Icon string `json:"icon"` TempMin string `json:"tempMin"` TempMax string `json:"tempMax"` } type currentWeather struct { Text string `json:"text"` Temp string `json:"temp"` } var ( weatherCacheMu sync.Mutex weatherCache map[string]time.Time weatherCacheData map[string]string ) func init() { weatherCache = make(map[string]time.Time) weatherCacheData = make(map[string]string) } func fetchAndPushWeather(city City) { // 5 分钟内同一城市不重复请求 weatherCacheMu.Lock() if last, ok := weatherCache[city.ID]; ok && time.Since(last) < 5*time.Minute { if cached, ok := weatherCacheData[city.ID]; ok { weatherCacheMu.Unlock() evalJS(fmt.Sprintf(`if(window.updateWeatherFromGo) window.updateWeatherFromGo(%s)`, cached)) return } } weatherCacheMu.Unlock() type weatherData struct { Current string `json:"current"` Hourly []hourlyItem `json:"hourly"` Daily []dailyItem `json:"daily"` City string `json:"city"` } wd := weatherData{} cityName := city.Name if city.Adm2 != city.Name { cityName = city.Adm2 + " " + city.Name } wd.City = cityName if now := fetchCurrentWeather(city.ID); now != nil { icon := getWeatherIcon(now.Text) wd.Current = fmt.Sprintf("📍 %s %s %s %s°C", cityName, icon, now.Text, now.Temp) } else { wd.Current = fmt.Sprintf("📍 %s ☀️ 晴 24°C", cityName) } wd.Hourly = fetchHourlyForecast(city.ID) wd.Daily = fetchDailyForecast(city.ID) jsonData, _ := json.Marshal(wd) weatherCacheMu.Lock() weatherCache[city.ID] = time.Now() weatherCacheData[city.ID] = string(jsonData) weatherCacheMu.Unlock() evalJS(fmt.Sprintf(`if(window.updateWeatherFromGo) window.updateWeatherFromGo(%s)`, string(jsonData))) log.Println("天气已推送:", wd.Current) } func fetchCurrentWeather(cityID string) *currentWeather { url := fmt.Sprintf(qweatherHost+"/v7/weather/now?location=%s&key=%s", cityID, qweatherKey) data, err := httpGet(url) if err != nil { return nil } var resp struct { Code string `json:"code"` Now struct { Text string `json:"text"` Temp string `json:"temp"` } `json:"now"` } if json.Unmarshal(data, &resp) != nil || resp.Code != "200" { return nil } return ¤tWeather{resp.Now.Text, resp.Now.Temp} } func fetchHourlyForecast(cityID string) []hourlyItem { url := fmt.Sprintf(qweatherHost+"/v7/weather/24h?location=%s&key=%s", cityID, qweatherKey) data, err := httpGet(url) if err != nil { return nil } var resp struct { Code string `json:"code"` Hourly []struct { FxTime string `json:"fxTime"` Temp string `json:"temp"` Text string `json:"text"` } `json:"hourly"` } if json.Unmarshal(data, &resp) != nil || resp.Code != "200" { return nil } var result []hourlyItem for i, h := range resp.Hourly { if i >= 8 { break } t := h.FxTime if idx := strings.Index(t, "T"); idx >= 0 { t = t[idx+1:] if len(t) >= 5 { t = t[:5] } } result = append(result, hourlyItem{t, h.Temp + "°", getWeatherIcon(h.Text)}) } return result } func fetchDailyForecast(cityID string) []dailyItem { url := fmt.Sprintf(qweatherHost+"/v7/weather/7d?location=%s&key=%s", cityID, qweatherKey) data, err := httpGet(url) if err != nil { return nil } var resp struct { Code string `json:"code"` Daily []struct { FxDate string `json:"fxDate"` TextDay string `json:"textDay"` TempMin string `json:"tempMin"` TempMax string `json:"tempMax"` } `json:"daily"` } if json.Unmarshal(data, &resp) != nil || resp.Code != "200" { return nil } weekdays := []string{"周日", "周一", "周二", "周三", "周四", "周五", "周六"} today := time.Now().Format("2006-01-02") var result []dailyItem for _, d := range resp.Daily { date := d.FxDate if date == today { date = "今天" } else { if t, err := time.Parse("2006-01-02", date); err == nil { date = weekdays[t.Weekday()] } } result = append(result, dailyItem{date, getWeatherIcon(d.TextDay), d.TempMin, d.TempMax}) } return result } func weatherLoop() { time.Sleep(3 * time.Second) city := getCurrentCity() log.Println("初始城市:", city.Name) fetchAndPushWeather(city) ticker := time.NewTicker(10 * time.Minute) for range ticker.C { city := getCurrentCity() fetchAndPushWeather(city) } }