推进活动系统最小成品闭环与游客体验
This commit is contained in:
132
backend/internal/httpapi/handlers/admin_asset_handler.go
Normal file
132
backend/internal/httpapi/handlers/admin_asset_handler.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"cmr-backend/internal/apperr"
|
||||
"cmr-backend/internal/httpx"
|
||||
"cmr-backend/internal/service"
|
||||
)
|
||||
|
||||
type AdminAssetHandler struct {
|
||||
service *service.AdminAssetService
|
||||
}
|
||||
|
||||
func NewAdminAssetHandler(service *service.AdminAssetService) *AdminAssetHandler {
|
||||
return &AdminAssetHandler{service: service}
|
||||
}
|
||||
|
||||
func (h *AdminAssetHandler) ListAssets(w http.ResponseWriter, r *http.Request) {
|
||||
result, err := h.service.ListManagedAssets(r.Context(), parseAdminLimit(r))
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *AdminAssetHandler) GetAsset(w http.ResponseWriter, r *http.Request) {
|
||||
result, err := h.service.GetManagedAsset(r.Context(), r.PathValue("assetPublicID"))
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *AdminAssetHandler) RegisterLink(w http.ResponseWriter, r *http.Request) {
|
||||
var req service.RegisterLinkAssetInput
|
||||
if err := httpx.DecodeJSON(r, &req); err != nil {
|
||||
httpx.WriteError(w, apperr.New(http.StatusBadRequest, "invalid_json", "invalid request body: "+err.Error()))
|
||||
return
|
||||
}
|
||||
result, err := h.service.RegisterExternalLink(r.Context(), req)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusCreated, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *AdminAssetHandler) UploadFile(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseMultipartForm(64 << 20); err != nil {
|
||||
httpx.WriteError(w, apperr.New(http.StatusBadRequest, "invalid_multipart", "invalid multipart form: "+err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
file, header, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
httpx.WriteError(w, apperr.New(http.StatusBadRequest, "file_required", "multipart file field 'file' is required"))
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
tmpFile, err := os.CreateTemp("", "cmr-upload-*"+header.Filename)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
tmpPath := tmpFile.Name()
|
||||
defer os.Remove(tmpPath)
|
||||
|
||||
hash := sha256.New()
|
||||
written, err := io.Copy(io.MultiWriter(tmpFile, hash), file)
|
||||
if err != nil {
|
||||
tmpFile.Close()
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
if err := tmpFile.Close(); err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
input := service.UploadAssetFileInput{
|
||||
AssetType: r.FormValue("assetType"),
|
||||
AssetCode: r.FormValue("assetCode"),
|
||||
Version: r.FormValue("version"),
|
||||
Title: stringPtrOrNil(r.FormValue("title")),
|
||||
ObjectDir: stringPtrOrNil(r.FormValue("objectDir")),
|
||||
FileName: header.Filename,
|
||||
ContentType: header.Header.Get("Content-Type"),
|
||||
FileSize: written,
|
||||
Checksum: hex.EncodeToString(hash.Sum(nil)),
|
||||
TempPath: tmpPath,
|
||||
Status: r.FormValue("status"),
|
||||
Metadata: parseMetadataJSON(r.FormValue("metadataJson")),
|
||||
}
|
||||
|
||||
result, err := h.service.UploadAssetFile(r.Context(), input)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusCreated, map[string]any{
|
||||
"data": result,
|
||||
"meta": map[string]any{
|
||||
"uploadedBytes": written,
|
||||
"checksumSha256": input.Checksum,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func stringPtrOrNil(value string) *string {
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
return &value
|
||||
}
|
||||
|
||||
func parseMetadataJSON(raw string) map[string]any {
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
var payload map[string]any
|
||||
_ = json.Unmarshal([]byte(raw), &payload)
|
||||
return payload
|
||||
}
|
||||
@@ -25,6 +25,15 @@ func (h *AdminProductionHandler) ListPlaces(w http.ResponseWriter, r *http.Reque
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *AdminProductionHandler) ListMapAssets(w http.ResponseWriter, r *http.Request) {
|
||||
result, err := h.service.ListMapAssets(r.Context(), parseAdminLimit(r))
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *AdminProductionHandler) CreatePlace(w http.ResponseWriter, r *http.Request) {
|
||||
var req service.CreateAdminPlaceInput
|
||||
if err := httpx.DecodeJSON(r, &req); err != nil {
|
||||
@@ -71,6 +80,20 @@ func (h *AdminProductionHandler) GetMapAsset(w http.ResponseWriter, r *http.Requ
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *AdminProductionHandler) UpdateMapAsset(w http.ResponseWriter, r *http.Request) {
|
||||
var req service.UpdateAdminMapAssetInput
|
||||
if err := httpx.DecodeJSON(r, &req); err != nil {
|
||||
httpx.WriteError(w, apperr.New(http.StatusBadRequest, "invalid_json", "invalid request body: "+err.Error()))
|
||||
return
|
||||
}
|
||||
result, err := h.service.UpdateMapAsset(r.Context(), r.PathValue("mapAssetPublicID"), req)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *AdminProductionHandler) CreateTileRelease(w http.ResponseWriter, r *http.Request) {
|
||||
var req service.CreateAdminTileReleaseInput
|
||||
if err := httpx.DecodeJSON(r, &req); err != nil {
|
||||
@@ -177,6 +200,34 @@ func (h *AdminProductionHandler) CreateRuntimeBinding(w http.ResponseWriter, r *
|
||||
httpx.WriteJSON(w, http.StatusCreated, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *AdminProductionHandler) ImportTileRelease(w http.ResponseWriter, r *http.Request) {
|
||||
var req service.ImportAdminTileReleaseInput
|
||||
if err := httpx.DecodeJSON(r, &req); err != nil {
|
||||
httpx.WriteError(w, apperr.New(http.StatusBadRequest, "invalid_json", "invalid request body: "+err.Error()))
|
||||
return
|
||||
}
|
||||
result, err := h.service.ImportTileRelease(r.Context(), req)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusCreated, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *AdminProductionHandler) ImportCourseSetKMLBatch(w http.ResponseWriter, r *http.Request) {
|
||||
var req service.ImportAdminCourseSetBatchInput
|
||||
if err := httpx.DecodeJSON(r, &req); err != nil {
|
||||
httpx.WriteError(w, apperr.New(http.StatusBadRequest, "invalid_json", "invalid request body: "+err.Error()))
|
||||
return
|
||||
}
|
||||
result, err := h.service.ImportCourseSetKMLBatch(r.Context(), req)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusCreated, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *AdminProductionHandler) GetRuntimeBinding(w http.ResponseWriter, r *http.Request) {
|
||||
result, err := h.service.GetRuntimeBinding(r.Context(), r.PathValue("runtimeBindingPublicID"))
|
||||
if err != nil {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
41
backend/internal/httpapi/handlers/map_experience_handler.go
Normal file
41
backend/internal/httpapi/handlers/map_experience_handler.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"cmr-backend/internal/httpx"
|
||||
"cmr-backend/internal/service"
|
||||
)
|
||||
|
||||
type MapExperienceHandler struct {
|
||||
service *service.MapExperienceService
|
||||
}
|
||||
|
||||
func NewMapExperienceHandler(service *service.MapExperienceService) *MapExperienceHandler {
|
||||
return &MapExperienceHandler{service: service}
|
||||
}
|
||||
|
||||
func (h *MapExperienceHandler) ListMaps(w http.ResponseWriter, r *http.Request) {
|
||||
limit := 20
|
||||
if raw := r.URL.Query().Get("limit"); raw != "" {
|
||||
if parsed, err := strconv.Atoi(raw); err == nil {
|
||||
limit = parsed
|
||||
}
|
||||
}
|
||||
result, err := h.service.ListMaps(r.Context(), service.ListExperienceMapsInput{Limit: limit})
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *MapExperienceHandler) GetMapDetail(w http.ResponseWriter, r *http.Request) {
|
||||
result, err := h.service.GetMapDetail(r.Context(), r.PathValue("mapAssetPublicID"))
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": result})
|
||||
}
|
||||
101
backend/internal/httpapi/handlers/ops_auth_handler.go
Normal file
101
backend/internal/httpapi/handlers/ops_auth_handler.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"cmr-backend/internal/apperr"
|
||||
"cmr-backend/internal/httpapi/middleware"
|
||||
"cmr-backend/internal/httpx"
|
||||
"cmr-backend/internal/service"
|
||||
)
|
||||
|
||||
type OpsAuthHandler struct {
|
||||
service *service.OpsAuthService
|
||||
}
|
||||
|
||||
func NewOpsAuthHandler(service *service.OpsAuthService) *OpsAuthHandler {
|
||||
return &OpsAuthHandler{service: service}
|
||||
}
|
||||
|
||||
func (h *OpsAuthHandler) SendSMSCode(w http.ResponseWriter, r *http.Request) {
|
||||
var req service.OpsSendSMSCodeInput
|
||||
if err := httpx.DecodeJSON(r, &req); err != nil {
|
||||
httpx.WriteError(w, apperr.New(http.StatusBadRequest, "invalid_json", "invalid request body"))
|
||||
return
|
||||
}
|
||||
result, err := h.service.SendSMSCode(r.Context(), req)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *OpsAuthHandler) Register(w http.ResponseWriter, r *http.Request) {
|
||||
var req service.OpsRegisterInput
|
||||
if err := httpx.DecodeJSON(r, &req); err != nil {
|
||||
httpx.WriteError(w, apperr.New(http.StatusBadRequest, "invalid_json", "invalid request body"))
|
||||
return
|
||||
}
|
||||
result, err := h.service.Register(r.Context(), req)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *OpsAuthHandler) LoginSMS(w http.ResponseWriter, r *http.Request) {
|
||||
var req service.OpsLoginSMSInput
|
||||
if err := httpx.DecodeJSON(r, &req); err != nil {
|
||||
httpx.WriteError(w, apperr.New(http.StatusBadRequest, "invalid_json", "invalid request body"))
|
||||
return
|
||||
}
|
||||
result, err := h.service.LoginSMS(r.Context(), req)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *OpsAuthHandler) Refresh(w http.ResponseWriter, r *http.Request) {
|
||||
var req service.OpsRefreshTokenInput
|
||||
if err := httpx.DecodeJSON(r, &req); err != nil {
|
||||
httpx.WriteError(w, apperr.New(http.StatusBadRequest, "invalid_json", "invalid request body"))
|
||||
return
|
||||
}
|
||||
result, err := h.service.Refresh(r.Context(), req)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *OpsAuthHandler) Logout(w http.ResponseWriter, r *http.Request) {
|
||||
var req service.OpsLogoutInput
|
||||
if err := httpx.DecodeJSON(r, &req); err != nil {
|
||||
httpx.WriteError(w, apperr.New(http.StatusBadRequest, "invalid_json", "invalid request body"))
|
||||
return
|
||||
}
|
||||
if err := h.service.Logout(r.Context(), req); err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"loggedOut": true}})
|
||||
}
|
||||
|
||||
func (h *OpsAuthHandler) Me(w http.ResponseWriter, r *http.Request) {
|
||||
auth := middleware.GetOpsAuthContext(r.Context())
|
||||
if auth == nil {
|
||||
httpx.WriteError(w, apperr.New(http.StatusUnauthorized, "unauthorized", "missing ops auth context"))
|
||||
return
|
||||
}
|
||||
result, err := h.service.GetMe(r.Context(), auth.OpsUserID)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": result})
|
||||
}
|
||||
25
backend/internal/httpapi/handlers/ops_summary_handler.go
Normal file
25
backend/internal/httpapi/handlers/ops_summary_handler.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"cmr-backend/internal/httpx"
|
||||
"cmr-backend/internal/service"
|
||||
)
|
||||
|
||||
type OpsSummaryHandler struct {
|
||||
service *service.OpsSummaryService
|
||||
}
|
||||
|
||||
func NewOpsSummaryHandler(service *service.OpsSummaryService) *OpsSummaryHandler {
|
||||
return &OpsSummaryHandler{service: service}
|
||||
}
|
||||
|
||||
func (h *OpsSummaryHandler) GetOverview(w http.ResponseWriter, r *http.Request) {
|
||||
result, err := h.service.GetOverview(r.Context())
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": result})
|
||||
}
|
||||
1681
backend/internal/httpapi/handlers/ops_workbench_handler.go
Normal file
1681
backend/internal/httpapi/handlers/ops_workbench_handler.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,78 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"cmr-backend/internal/apperr"
|
||||
"cmr-backend/internal/httpx"
|
||||
"cmr-backend/internal/service"
|
||||
)
|
||||
|
||||
type PublicExperienceHandler struct {
|
||||
service *service.PublicExperienceService
|
||||
}
|
||||
|
||||
func NewPublicExperienceHandler(service *service.PublicExperienceService) *PublicExperienceHandler {
|
||||
return &PublicExperienceHandler{service: service}
|
||||
}
|
||||
|
||||
func (h *PublicExperienceHandler) ListMaps(w http.ResponseWriter, r *http.Request) {
|
||||
limit := 20
|
||||
if raw := r.URL.Query().Get("limit"); raw != "" {
|
||||
if parsed, err := strconv.Atoi(raw); err == nil {
|
||||
limit = parsed
|
||||
}
|
||||
}
|
||||
result, err := h.service.ListMaps(r.Context(), service.ListExperienceMapsInput{Limit: limit})
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *PublicExperienceHandler) GetMapDetail(w http.ResponseWriter, r *http.Request) {
|
||||
result, err := h.service.GetMapDetail(r.Context(), r.PathValue("mapAssetPublicID"))
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *PublicExperienceHandler) GetEventDetail(w http.ResponseWriter, r *http.Request) {
|
||||
result, err := h.service.GetEventDetail(r.Context(), r.PathValue("eventPublicID"))
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *PublicExperienceHandler) GetEventPlay(w http.ResponseWriter, r *http.Request) {
|
||||
result, err := h.service.GetEventPlay(r.Context(), service.PublicEventPlayInput{
|
||||
EventPublicID: r.PathValue("eventPublicID"),
|
||||
})
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *PublicExperienceHandler) Launch(w http.ResponseWriter, r *http.Request) {
|
||||
var req service.PublicLaunchEventInput
|
||||
if err := httpx.DecodeJSON(r, &req); err != nil {
|
||||
httpx.WriteError(w, apperr.New(http.StatusBadRequest, "invalid_json", "invalid request body"))
|
||||
return
|
||||
}
|
||||
req.EventPublicID = r.PathValue("eventPublicID")
|
||||
|
||||
result, err := h.service.LaunchEvent(r.Context(), req)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": result})
|
||||
}
|
||||
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