推进活动系统最小成品闭环与游客体验
This commit is contained in:
162
backend/internal/httpapi/handlers/region_options_handler.go
Normal file
162
backend/internal/httpapi/handlers/region_options_handler.go
Normal file
@@ -0,0 +1,162 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user