Files
2026-11-11 08:53:14 +08:00

147 lines
4.1 KiB
Go

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 ""
}