init: initial commit
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
package location
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// StringOrArray ########################### 核心:兼容字符串/数组的自定义类型 ###########################
|
||||
type StringOrArray string
|
||||
|
||||
// UnmarshalJSON 处理字符串/数组两种格式
|
||||
func (s *StringOrArray) UnmarshalJSON(data []byte) error {
|
||||
// 尝试解析为纯字符串
|
||||
var str string
|
||||
if err := json.Unmarshal(data, &str); err == nil {
|
||||
*s = StringOrArray(str)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 尝试解析为字符串数组
|
||||
var strArr []string
|
||||
if err := json.Unmarshal(data, &strArr); err != nil {
|
||||
return fmt.Errorf("字段解析失败: %v, 原始数据: %s", err, string(data))
|
||||
}
|
||||
|
||||
// 数组取第一个元素(高德返回的数组通常仅1个元素)
|
||||
if len(strArr) > 0 {
|
||||
*s = StringOrArray(strArr[0])
|
||||
} else {
|
||||
*s = StringOrArray("")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// String 转为普通字符串,方便使用
|
||||
func (s StringOrArray) String() string {
|
||||
return string(s)
|
||||
}
|
||||
|
||||
// ########################### 严格对齐高德API的结构体 ###########################
|
||||
// AMapEntity 最终返回的实体(对应Java的AMapEntity)
|
||||
type AMapEntity struct {
|
||||
AddressComponent AddressComponent `json:"addresscomponent"`
|
||||
Address StringOrArray `json:"formatted_address"` // 改为兼容类型
|
||||
}
|
||||
|
||||
// StreetNumber 门牌号嵌套对象(高德固定返回对象)
|
||||
type StreetNumber struct {
|
||||
Number StringOrArray `json:"number"` // 门牌号
|
||||
Location StringOrArray `json:"location"` // 经纬度
|
||||
Direction StringOrArray `json:"direction"` // 方向
|
||||
Distance StringOrArray `json:"distance"` // 距离
|
||||
Street StringOrArray `json:"street"` // 所属街道
|
||||
}
|
||||
|
||||
// AddressComponent 地址组件(全量兼容)
|
||||
type AddressComponent struct {
|
||||
Province StringOrArray `json:"province"` // 省(字符串/数组)
|
||||
City StringOrArray `json:"city"` // 市(字符串/数组)
|
||||
District StringOrArray `json:"district"` // 区(字符串/数组)
|
||||
Township StringOrArray `json:"township"` // 乡镇(纯字符串)
|
||||
Street StringOrArray `json:"street"` // 街道(纯字符串)
|
||||
StreetNumber StreetNumber `json:"streetnumber"` // 门牌号(对象)
|
||||
Adcode StringOrArray `json:"adcode"` // 行政区划编码(纯字符串)
|
||||
Country StringOrArray `json:"country"` // 国家(纯字符串)
|
||||
CountryCode StringOrArray `json:"countrycode"` // 国家编码(纯字符串)
|
||||
}
|
||||
|
||||
// AMapResponse 高德API顶层响应
|
||||
type AMapResponse struct {
|
||||
Regeocode Regeocode `json:"regeocode"` // 核心数据
|
||||
Status string `json:"status"` // 1=成功,0=失败
|
||||
Info string `json:"info"` // 错误信息
|
||||
Infocode string `json:"infocode"` // 错误码(精准定位问题)
|
||||
}
|
||||
|
||||
// Regeocode 逆地理编码核心数据
|
||||
type Regeocode struct {
|
||||
FormattedAddress StringOrArray `json:"formatted_address"` // 改为兼容类型
|
||||
AddressComponent AddressComponent `json:"addresscomponent"` // 地址组件
|
||||
}
|
||||
|
||||
// ########################### 配置常量 ###########################
|
||||
const (
|
||||
appKey = "1b8dd8848bcc062ff1f2cec6db683673" // 替换为你的有效Key
|
||||
amapApiUrl = "https://restapi.amap.com/v3/geocode/regeo"
|
||||
)
|
||||
|
||||
// Point2code 经纬度转地址(完整错误处理+兼容)
|
||||
func Point2code(longitude, latitude float32) (*AMapEntity, error) {
|
||||
// 1. 基础参数校验
|
||||
if longitude == 0 || latitude == 0 {
|
||||
return nil, errors.New("经纬度不能为0")
|
||||
}
|
||||
|
||||
// 2. 构建请求URL
|
||||
baseURL, err := url.Parse(amapApiUrl)
|
||||
if err != nil {
|
||||
log.Printf("URL解析失败: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 设置查询参数
|
||||
params := url.Values{}
|
||||
params.Set("key", appKey)
|
||||
params.Set("location", fmt.Sprintf("%f,%f", longitude, latitude))
|
||||
baseURL.RawQuery = params.Encode()
|
||||
|
||||
// 3. 全局HTTP客户端(建议单例复用)
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
|
||||
// 4. 发送GET请求
|
||||
req, err := http.NewRequest(http.MethodGet, baseURL.String(), nil)
|
||||
if err != nil {
|
||||
log.Printf("请求构建失败: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Printf("请求发送失败: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close() // 强制关闭响应体,避免内存泄漏
|
||||
|
||||
// 5. 校验HTTP状态码
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
errMsg := fmt.Sprintf("HTTP请求失败,状态码: %d", resp.StatusCode)
|
||||
log.Println(errMsg)
|
||||
return nil, errors.New(errMsg)
|
||||
}
|
||||
|
||||
// 6. 读取响应体
|
||||
bodyBytes, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Printf("响应体读取失败: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 7. 解析JSON响应
|
||||
var amapResp AMapResponse
|
||||
if err := json.Unmarshal(bodyBytes, &amapResp); err != nil {
|
||||
log.Printf("JSON解析失败: %v, 响应内容: %s", err, string(bodyBytes))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 8. 校验高德API业务状态
|
||||
if amapResp.Status != "1" {
|
||||
errMsg := fmt.Sprintf("高德API返回失败: status=%s, info=%s, infocode=%s",
|
||||
amapResp.Status, amapResp.Info, amapResp.Infocode)
|
||||
log.Println(errMsg)
|
||||
return nil, errors.New(errMsg)
|
||||
}
|
||||
|
||||
// 9. 构造最终返回实体
|
||||
result := &AMapEntity{
|
||||
AddressComponent: amapResp.Regeocode.AddressComponent,
|
||||
Address: amapResp.Regeocode.FormattedAddress,
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package location
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ########################### 1. 结构体定义(对齐高德天气API返回格式) ###########################
|
||||
// WeatherResponse 高德天气API顶层响应结构体
|
||||
type WeatherResponse struct {
|
||||
Status string `json:"status"` // 1=成功,0=失败
|
||||
Info string `json:"info"` // 错误信息
|
||||
Infocode string `json:"infocode"` // 错误码
|
||||
Lives []LiveWeather `json:"lives"` // 实时天气(数组,仅1条数据)
|
||||
Forecasts []Forecast `json:"forecasts"` // 天气预报(数组,仅1条数据)
|
||||
}
|
||||
|
||||
// LiveWeather 实时天气结构体
|
||||
type LiveWeather struct {
|
||||
Province string `json:"province"` // 省份
|
||||
City string `json:"city"` // 城市
|
||||
Adcode string `json:"adcode"` // 行政区划编码
|
||||
Weather string `json:"weather"` // 天气现象(如晴、阴)
|
||||
Temperature string `json:"temperature"` // 实时气温(℃)
|
||||
WindDirection string `json:"winddirection"` // 风向(如东、西南)
|
||||
WindPower string `json:"windpower"` // 风力(如3级)
|
||||
Humidity string `json:"humidity"` // 湿度(%)
|
||||
ReportTime string `json:"reporttime"` // 数据更新时间
|
||||
}
|
||||
|
||||
// Forecast 天气预报顶层结构体(包含多日预报)
|
||||
type Forecast struct {
|
||||
Province string `json:"province"` // 省份
|
||||
City string `json:"city"` // 城市
|
||||
Adcode string `json:"adcode"` // 行政区划编码
|
||||
ReportTime string `json:"reporttime"` // 预报发布时间
|
||||
Casts []ForecastDay `json:"casts"` // 每日预报(未来3天)
|
||||
}
|
||||
|
||||
// ForecastDay 单日天气预报
|
||||
type ForecastDay struct {
|
||||
Date string `json:"date"` // 日期(yyyy-MM-dd)
|
||||
Week string `json:"week"` // 星期(1=周一,7=周日)
|
||||
DayWeather string `json:"dayweather"` // 白天天气
|
||||
NightWeather string `json:"nightweather"` // 夜间天气
|
||||
DayTemp string `json:"daytemp"` // 白天温度
|
||||
NightTemp string `json:"nighttemp"` // 夜间温度
|
||||
DayWindDir string `json:"daywinddir"` // 白天风向
|
||||
NightWindDir string `json:"nightwinddir"` // 夜间风向
|
||||
DayWindPower string `json:"daywindpower"` // 白天风力
|
||||
NightWindPower string `json:"nightwindpower"` // 夜间风力
|
||||
}
|
||||
|
||||
// ########################### 2. 配置常量 ###########################
|
||||
const (
|
||||
amapWeatherApi = "https://restapi.amap.com/v3/weather/weatherInfo"
|
||||
)
|
||||
|
||||
// GetWeather 根据行政区划编码查询天气
|
||||
// adcode: 行政区划编码(如110101=北京市东城区)
|
||||
// extensions: 查询类型(base=实时,all=实时+预报)
|
||||
func GetWeather(adcode string, extensions string) (*WeatherResponse, error) {
|
||||
// 1. 参数校验
|
||||
if adcode == "" {
|
||||
return nil, errors.New("行政区划编码adcode不能为空")
|
||||
}
|
||||
if extensions == "" {
|
||||
extensions = "base" // 默认查实时天气
|
||||
}
|
||||
if extensions != "base" && extensions != "all" {
|
||||
return nil, errors.New("extensions仅支持base(实时)或all(实时+预报)")
|
||||
}
|
||||
|
||||
// 2. 构建请求URL和参数
|
||||
baseURL, err := url.Parse(amapWeatherApi)
|
||||
if err != nil {
|
||||
log.Printf("解析基础URL失败: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 设置查询参数
|
||||
params := url.Values{}
|
||||
params.Set("key", appKey)
|
||||
params.Set("city", adcode) // 核心参数:行政区划编码
|
||||
params.Set("extensions", extensions)
|
||||
params.Set("output", "json") // 固定返回JSON格式
|
||||
baseURL.RawQuery = params.Encode()
|
||||
|
||||
// 3. 发送HTTP请求(全局客户端,复用连接)
|
||||
client := &http.Client{
|
||||
Timeout: 10 * time.Second, // 10秒超时
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodGet, baseURL.String(), nil)
|
||||
if err != nil {
|
||||
log.Printf("构建请求失败: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Printf("发送请求失败: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close() // 强制关闭响应体
|
||||
|
||||
// 4. 检查HTTP状态码
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
errMsg := fmt.Sprintf("请求失败,状态码: %d", resp.StatusCode)
|
||||
log.Println(errMsg)
|
||||
return nil, errors.New(errMsg)
|
||||
}
|
||||
|
||||
// 5. 读取响应体
|
||||
bodyBytes, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Printf("读取响应体失败: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 6. 解析JSON响应
|
||||
var weatherResp WeatherResponse
|
||||
if err := json.Unmarshal(bodyBytes, &weatherResp); err != nil {
|
||||
log.Printf("解析JSON失败: %v, 响应内容: %s", err, string(bodyBytes))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 7. 检查API业务状态
|
||||
if weatherResp.Status != "1" {
|
||||
errMsg := fmt.Sprintf("高德天气API返回失败: status=%s, info=%s, infocode=%s",
|
||||
weatherResp.Status, weatherResp.Info, weatherResp.Infocode)
|
||||
log.Println(errMsg)
|
||||
return nil, errors.New(errMsg)
|
||||
}
|
||||
|
||||
// 8. 校验返回数据非空
|
||||
if extensions == "base" && len(weatherResp.Lives) == 0 {
|
||||
return nil, errors.New("未查询到实时天气数据")
|
||||
}
|
||||
if extensions == "all" && (len(weatherResp.Lives) == 0 || len(weatherResp.Forecasts) == 0) {
|
||||
return nil, errors.New("未查询到天气数据(实时/预报)")
|
||||
}
|
||||
|
||||
return &weatherResp, nil
|
||||
}
|
||||
|
||||
// ########################### 4. 测试示例 ###########################
|
||||
func main() {
|
||||
// 示例1:查询北京市东城区(adcode=110101)实时天气
|
||||
adcode := "110101"
|
||||
// 可选:extensions="all" 查询实时+未来3天预报
|
||||
weatherResp, err := GetWeather(adcode, "base")
|
||||
if err != nil {
|
||||
log.Fatalf("查询天气失败: %v", err)
|
||||
}
|
||||
|
||||
// 打印实时天气
|
||||
fmt.Println("=== 实时天气 ===")
|
||||
live := weatherResp.Lives[0] // 实时天气数组仅1条数据
|
||||
fmt.Printf("省份:%s\n", live.Province)
|
||||
fmt.Printf("城市:%s\n", live.City)
|
||||
fmt.Printf("行政区划编码:%s\n", live.Adcode)
|
||||
fmt.Printf("天气:%s\n", live.Weather)
|
||||
fmt.Printf("实时气温:%s℃\n", live.Temperature)
|
||||
fmt.Printf("风向:%s\n", live.WindDirection)
|
||||
fmt.Printf("风力:%s级\n", live.WindPower)
|
||||
fmt.Printf("湿度:%s%%\n", live.Humidity)
|
||||
fmt.Printf("数据更新时间:%s\n", live.ReportTime)
|
||||
|
||||
// 若查询的是all(实时+预报),打印预报数据
|
||||
// if len(weatherResp.Forecasts) > 0 {
|
||||
// fmt.Println("\n=== 未来3天预报 ===")
|
||||
// forecast := weatherResp.Forecasts[0]
|
||||
// for _, day := range forecast.Casts {
|
||||
// fmt.Printf("\n日期:%s(星期%s)\n", day.Date, day.Week)
|
||||
// fmt.Printf("白天天气:%s,温度:%s℃\n", day.DayWeather, day.DayTemp)
|
||||
// fmt.Printf("夜间天气:%s,温度:%s℃\n", day.NightWeather, day.NightTemp)
|
||||
// fmt.Printf("白天风向/风力:%s/%s级\n", day.DayWindDir, day.DayWindPower)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
Reference in New Issue
Block a user