147 lines
4.4 KiB
Go
147 lines
4.4 KiB
Go
package environment
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
var ErrNotConfigured = errors.New("位置和天气服务暂未配置")
|
|
var ErrCoordinates = errors.New("定位坐标无效")
|
|
|
|
type Weather struct {
|
|
Description string `json:"description"`
|
|
Temperature string `json:"temperature"`
|
|
ReportedAt string `json:"reported_at"`
|
|
}
|
|
type Result struct {
|
|
LocationName string `json:"location_name"`
|
|
City string `json:"city"`
|
|
District string `json:"district"`
|
|
Weather *Weather `json:"weather"`
|
|
}
|
|
type cached struct {
|
|
result Result
|
|
expires time.Time
|
|
}
|
|
type Client struct {
|
|
key string
|
|
baseURL string
|
|
http *http.Client
|
|
mu sync.Mutex
|
|
cache map[string]cached
|
|
}
|
|
|
|
func New(key string) *Client {
|
|
return &Client{key: key, baseURL: "https://restapi.amap.com", http: &http.Client{Timeout: 5 * time.Second}, cache: make(map[string]cached)}
|
|
}
|
|
|
|
// AMap returns [] instead of an empty string for some address components.
|
|
func addressText(raw json.RawMessage) string {
|
|
var text string
|
|
_ = json.Unmarshal(raw, &text)
|
|
return text
|
|
}
|
|
func (c *Client) get(ctx context.Context, path string, params url.Values, target any) error {
|
|
params.Set("key", c.key)
|
|
params.Set("output", "JSON")
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+path+"?"+params.Encode(), nil)
|
|
if err != nil {
|
|
return errors.New("位置服务请求失败")
|
|
}
|
|
res, err := c.http.Do(req)
|
|
// Never return the transport error: it may contain the URL and API key.
|
|
if err != nil {
|
|
return errors.New("位置服务暂时不可用")
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusOK {
|
|
return errors.New("位置服务暂时不可用")
|
|
}
|
|
if err := json.NewDecoder(io.LimitReader(res.Body, 1<<20)).Decode(target); err != nil {
|
|
return errors.New("位置服务响应无效")
|
|
}
|
|
return nil
|
|
}
|
|
func (c *Client) Lookup(ctx context.Context, latitude, longitude float64) (Result, error) {
|
|
if math.IsNaN(latitude) || math.IsNaN(longitude) || math.IsInf(latitude, 0) || math.IsInf(longitude, 0) || latitude < -90 || latitude > 90 || longitude < -180 || longitude > 180 {
|
|
return Result{}, ErrCoordinates
|
|
}
|
|
if c.key == "" {
|
|
return Result{}, ErrNotConfigured
|
|
}
|
|
cacheKey := fmt.Sprintf("%.3f,%.3f", longitude, latitude)
|
|
c.mu.Lock()
|
|
hit, found := c.cache[cacheKey]
|
|
c.mu.Unlock()
|
|
if found && time.Now().Before(hit.expires) {
|
|
return hit.result, nil
|
|
}
|
|
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
|
defer cancel()
|
|
var geo struct {
|
|
Status string `json:"status"`
|
|
Regeocode struct {
|
|
Address struct {
|
|
Province json.RawMessage `json:"province"`
|
|
City json.RawMessage `json:"city"`
|
|
District json.RawMessage `json:"district"`
|
|
Adcode string `json:"adcode"`
|
|
} `json:"addressComponent"`
|
|
} `json:"regeocode"`
|
|
}
|
|
err := c.get(ctx, "/v3/geocode/regeo", url.Values{"location": {fmt.Sprintf("%.6f,%.6f", longitude, latitude)}, "extensions": {"base"}}, &geo)
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
if geo.Status != "1" {
|
|
return Result{}, errors.New("所在城市获取失败,请稍后重试")
|
|
}
|
|
address := geo.Regeocode.Address
|
|
city := addressText(address.City)
|
|
if city == "" {
|
|
city = addressText(address.Province)
|
|
}
|
|
district := addressText(address.District)
|
|
if city == "" || address.Adcode == "" {
|
|
return Result{}, errors.New("当前位置暂不支持天气查询")
|
|
}
|
|
result := Result{City: city, District: district, LocationName: city}
|
|
if district != "" && district != city {
|
|
result.LocationName += " · " + district
|
|
}
|
|
var weather struct {
|
|
Status string `json:"status"`
|
|
Lives []struct {
|
|
Description string `json:"weather"`
|
|
Temperature string `json:"temperature"`
|
|
ReportedAt string `json:"reporttime"`
|
|
} `json:"lives"`
|
|
}
|
|
err = c.get(ctx, "/v3/weather/weatherInfo", url.Values{"city": {address.Adcode}, "extensions": {"base"}}, &weather)
|
|
if err == nil && weather.Status == "1" && len(weather.Lives) > 0 {
|
|
live := weather.Lives[0]
|
|
if strings.TrimSpace(live.Description) != "" && live.Temperature != "" {
|
|
result.Weather = &Weather{live.Description, live.Temperature, live.ReportedAt}
|
|
}
|
|
}
|
|
// Keep the city even if the weather provider fails; retry missing weather next time.
|
|
if result.Weather != nil {
|
|
c.mu.Lock()
|
|
if len(c.cache) >= 256 {
|
|
clear(c.cache)
|
|
}
|
|
c.cache[cacheKey] = cached{result, time.Now().Add(15 * time.Minute)}
|
|
c.mu.Unlock()
|
|
}
|
|
return result, nil
|
|
}
|