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