package controllers import ( "bytes" "encoding/json" "io" "net/http" "strconv" "strings" "time" beego "github.com/beego/beego/v2/server/web" ) // BackendWeatherController 天气实时数据代理。 // // 数据源:中国天气网 d1.weather.com.cn(国内真实数据)。 // 该接口有防盗链,浏览器直连会被 Referer 校验拦掉、也会跨域, // 因此由服务端带 Referer 抓取后统一返回 JSON。 type BackendWeatherController struct { beego.Controller } // 常用城市编码(中国天气网 citycode),未命中时按入参 code 直取 var weatherCityCodes = map[string]string{ "连云港": "101191001", "南京": "101190101", "苏州": "101190401", "无锡": "101190201", "常州": "101191101", "南通": "101190501", "扬州": "101190601", "徐州": "101190801", "盐城": "101190701", "淮安": "101190901", "镇江": "101190301", "泰州": "101191201", "宿迁": "101191301", "北京": "101010100", "上海": "101020100", "广州": "101280101", "深圳": "101280601", "杭州": "101210101", } const defaultWeatherCity = "连云港" // Now GET /backend/weather/now?city=连云港 或 ?code=101191201 func (c *BackendWeatherController) Now() { city := strings.TrimSpace(c.GetString("city")) code := strings.TrimSpace(c.GetString("code")) if code == "" { code = weatherCityCodes[city] } if code == "" { code = weatherCityCodes[defaultWeatherCity] city = defaultWeatherCity } client := &http.Client{Timeout: 8 * time.Second} ts := strconv.FormatInt(time.Now().UnixMilli(), 10) referer := "https://www.weather.com.cn/weather1d/" + code + ".shtml" // 主数据源:dingzhi(含天气现象 + 气温)。中国天气网已拒绝 http 明文连接,必须走 https。 info := weatherFetchJSON(client, "https://d1.weather.com.cn/dingzhi/"+code+".html?_="+ts, referer) if wi, ok := info["weatherinfo"].(map[string]interface{}); ok { info = wi } // 兜底:sk_2d(实况气温,无天气现象) if len(info) == 0 { info = weatherFetchJSON(client, "https://d1.weather.com.cn/sk_2d/"+code+".html?_="+ts, referer) } if len(info) == 0 { c.Ctx.Output.SetStatus(http.StatusBadGateway) c.Data["json"] = map[string]interface{}{"code": 502, "msg": "天气数据获取失败,请稍后重试"} _ = c.ServeJSON() return } cityName := weatherStr(info, "cityname", "city", "name") if cityName == "" { cityName = city } temp := strings.TrimSuffix(weatherStr(info, "temp", "real"), "℃") text := weatherStr(info, "weather", "weather1") c.Data["json"] = map[string]interface{}{ "code": 200, "msg": "ok", "data": map[string]interface{}{ "city": cityName, "temp": temp, "text": text, "update": weatherStr(info, "time", "fctime", "date"), "code": code, }, } _ = c.ServeJSON() } // weatherFetchJSON 带 Referer/UA 抓取中国天气网页面并解析其中的 JSON 对象 func weatherFetchJSON(client *http.Client, url, referer string) map[string]interface{} { req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { return nil } req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36") req.Header.Set("Referer", referer) req.Header.Set("Accept", "*/*") resp, err := client.Do(req) if err != nil { return nil } defer resp.Body.Close() body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) if err != nil { return nil } // 返回体形如:var cityDZ101191001 ={"weatherinfo":{...}};var alarmDZ101191001 ={"w":[]} // 从第一个 '{' 起用 json.Decoder 只解出第一个完整对象(正则贪婪匹配会跨越多个对象导致解析失败) idx := bytes.IndexByte(body, '{') if idx < 0 { return nil } var out map[string]interface{} if json.NewDecoder(bytes.NewReader(body[idx:])).Decode(&out) != nil { return nil } return out } // weatherStr 按候选字段顺序取第一个非空字符串值 func weatherStr(m map[string]interface{}, keys ...string) string { for _, k := range keys { if v, ok := m[k]; ok { if s, ok := v.(string); ok && strings.TrimSpace(s) != "" { return strings.TrimSpace(s) } } } return "" }