163 lines
4.4 KiB
Go
163 lines
4.4 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
|
|
"cmr-backend/internal/apperr"
|
|
"cmr-backend/internal/httpx"
|
|
)
|
|
|
|
type RegionOptionsHandler struct {
|
|
client *http.Client
|
|
mu sync.Mutex
|
|
cache []regionProvince
|
|
}
|
|
|
|
type regionProvince struct {
|
|
Code string `json:"code"`
|
|
Name string `json:"name"`
|
|
Cities []regionCity `json:"cities"`
|
|
}
|
|
|
|
type regionCity struct {
|
|
Code string `json:"code"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
type remoteProvince struct {
|
|
Code string `json:"code"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
type remoteCity struct {
|
|
Code string `json:"code"`
|
|
Name string `json:"name"`
|
|
Province string `json:"province"`
|
|
}
|
|
|
|
func NewRegionOptionsHandler() *RegionOptionsHandler {
|
|
return &RegionOptionsHandler{
|
|
client: &http.Client{Timeout: 12 * time.Second},
|
|
}
|
|
}
|
|
|
|
func (h *RegionOptionsHandler) Get(w http.ResponseWriter, r *http.Request) {
|
|
items, err := h.load(r.Context())
|
|
if err != nil {
|
|
httpx.WriteError(w, err)
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": items})
|
|
}
|
|
|
|
func (h *RegionOptionsHandler) load(ctx context.Context) ([]regionProvince, error) {
|
|
h.mu.Lock()
|
|
if len(h.cache) > 0 {
|
|
cached := h.cache
|
|
h.mu.Unlock()
|
|
return cached, nil
|
|
}
|
|
h.mu.Unlock()
|
|
|
|
// Data source:
|
|
// https://github.com/uiwjs/province-city-china
|
|
// Using province + city JSON only, then reducing to the province/city structure
|
|
// needed by ops workbench location management.
|
|
provinces, err := h.fetchProvinces(ctx, "https://unpkg.com/province-city-china/dist/province.json")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cities, err := h.fetchCities(ctx, "https://unpkg.com/province-city-china/dist/city.json")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cityMap := make(map[string][]regionCity)
|
|
for _, item := range cities {
|
|
if item.Province == "" || item.Code == "" {
|
|
continue
|
|
}
|
|
fullCode := item.Province + item.Code + "00"
|
|
cityMap[item.Province] = append(cityMap[item.Province], regionCity{
|
|
Code: fullCode,
|
|
Name: item.Name,
|
|
})
|
|
}
|
|
for key := range cityMap {
|
|
sort.Slice(cityMap[key], func(i, j int) bool { return cityMap[key][i].Code < cityMap[key][j].Code })
|
|
}
|
|
|
|
items := make([]regionProvince, 0, len(provinces))
|
|
for _, item := range provinces {
|
|
if len(item.Code) < 2 {
|
|
continue
|
|
}
|
|
provinceCode := item.Code[:2]
|
|
province := regionProvince{
|
|
Code: item.Code,
|
|
Name: item.Name,
|
|
}
|
|
if entries := cityMap[provinceCode]; len(entries) > 0 {
|
|
province.Cities = entries
|
|
} else {
|
|
// 直辖市 / 特殊地区没有单独的地级市列表时,退化成自身即可。
|
|
province.Cities = []regionCity{{
|
|
Code: item.Code,
|
|
Name: item.Name,
|
|
}}
|
|
}
|
|
items = append(items, province)
|
|
}
|
|
|
|
h.mu.Lock()
|
|
h.cache = items
|
|
h.mu.Unlock()
|
|
return items, nil
|
|
}
|
|
|
|
func (h *RegionOptionsHandler) fetchProvinces(ctx context.Context, url string) ([]remoteProvince, error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, apperr.New(http.StatusBadGateway, "region_source_unavailable", "省市数据源不可用")
|
|
}
|
|
resp, err := h.client.Do(req)
|
|
if err != nil {
|
|
return nil, apperr.New(http.StatusBadGateway, "region_source_unavailable", "省市数据源不可用")
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, apperr.New(http.StatusBadGateway, "region_source_unavailable", fmt.Sprintf("省级数据拉取失败: %d", resp.StatusCode))
|
|
}
|
|
var items []remoteProvince
|
|
if err := json.NewDecoder(resp.Body).Decode(&items); err != nil {
|
|
return nil, apperr.New(http.StatusBadGateway, "region_source_invalid", "省级数据格式无效")
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
func (h *RegionOptionsHandler) fetchCities(ctx context.Context, url string) ([]remoteCity, error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, apperr.New(http.StatusBadGateway, "region_source_unavailable", "省市数据源不可用")
|
|
}
|
|
resp, err := h.client.Do(req)
|
|
if err != nil {
|
|
return nil, apperr.New(http.StatusBadGateway, "region_source_unavailable", "省市数据源不可用")
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, apperr.New(http.StatusBadGateway, "region_source_unavailable", fmt.Sprintf("市级数据拉取失败: %d", resp.StatusCode))
|
|
}
|
|
var items []remoteCity
|
|
if err := json.NewDecoder(resp.Body).Decode(&items); err != nil {
|
|
return nil, apperr.New(http.StatusBadGateway, "region_source_invalid", "市级数据格式无效")
|
|
}
|
|
return items, nil
|
|
}
|