package main import ( "encoding/json" "fmt" "io" "log" "math" "net/http" "net/url" "os" "strings" "github.com/gin-gonic/gin" ) // Config 配置结构 type Config struct { AmapKey string `json:"amap_key"` // Web服务API Key AmapJsKey string `json:"amap_js_key"` // JS API Key AmapJsSecret string `json:"amap_js_secret"` // JS API 安全密钥 Port string `json:"port"` } // Coordinate 坐标结构 type Coordinate struct { Lng float64 `json:"lng"` Lat float64 `json:"lat"` } // Location 位置信息 type Location struct { Name string `json:"name"` Address string `json:"address"` Coordinate Coordinate `json:"coordinate"` } // CenterRequest 计算中心点请求 type CenterRequest struct { Locations []Location `json:"locations"` } // SearchRequest 搜索请求 type SearchRequest struct { Center Coordinate `json:"center"` Keywords string `json:"keywords"` Radius int `json:"radius"` // 搜索半径,单位米 } // POI 兴趣点 type POI struct { ID string `json:"id"` Name string `json:"name"` Type string `json:"type"` Address string `json:"address"` Location Coordinate `json:"location"` Distance string `json:"distance"` Tel string `json:"tel"` } // AmapGeoResponse 高德地理编码响应 type AmapGeoResponse struct { Status string `json:"status"` Info string `json:"info"` Count string `json:"count"` Geocodes []struct { FormattedAddress string `json:"formatted_address"` Location string `json:"location"` Level string `json:"level"` } `json:"geocodes"` } // AmapPOI 高德POI项 type AmapPOI struct { ID string `json:"id"` Name string `json:"name"` Type string `json:"type"` Address interface{} `json:"address"` // 可能是字符串或空数组 Location string `json:"location"` Distance string `json:"distance"` Tel interface{} `json:"tel"` // 可能是字符串或空数组 } // AmapPOIResponse 高德POI搜索响应 type AmapPOIResponse struct { Status string `json:"status"` Info string `json:"info"` Count string `json:"count"` Suggestion struct { Keywords []interface{} `json:"keywords"` } `json:"suggestion"` Pois []AmapPOI `json:"pois"` } // AmapTip 高德输入提示项 type AmapTip struct { ID interface{} `json:"id"` // 可能是字符串或空数组 Name string `json:"name"` District string `json:"district"` Address interface{} `json:"address"` // 可能是字符串或空数组 Location interface{} `json:"location"` // 可能是字符串或空数组 } // AmapTipsResponse 高德输入提示响应 type AmapTipsResponse struct { Status string `json:"status"` Info string `json:"info"` Count string `json:"count"` Tips []AmapTip `json:"tips"` } var config Config func main() { // 加载配置 loadConfig() // 设置Gin模式 gin.SetMode(gin.ReleaseMode) r := gin.Default() // 静态文件服务 r.Static("/static", "./static") r.LoadHTMLGlob("templates/*") // 路由 r.GET("/", indexHandler) r.GET("/api/config", getConfigHandler) r.POST("/api/geocode", geocodeHandler) r.POST("/api/center", calculateCenterHandler) r.POST("/api/search", searchPOIHandler) r.GET("/api/tips", getTipsHandler) port := config.Port if port == "" { port = "8080" } log.Printf("服务启动在 http://localhost:%s", port) r.Run(":" + port) } func loadConfig() { // 尝试从配置文件加载 file, err := os.Open("config.json") if err == nil { defer file.Close() decoder := json.NewDecoder(file) err = decoder.Decode(&config) if err != nil { log.Printf("解析配置文件失败: %v", err) } } // 环境变量覆盖 if key := os.Getenv("AMAP_KEY"); key != "" { config.AmapKey = key } if port := os.Getenv("PORT"); port != "" { config.Port = port } if config.Port == "" { config.Port = "8080" } } func indexHandler(c *gin.Context) { c.HTML(http.StatusOK, "index.html", nil) } func getConfigHandler(c *gin.Context) { // 返回前端需要的配置 jsKey := config.AmapJsKey if jsKey == "" { jsKey = config.AmapKey // 兼容:如果没有单独配置JS Key,使用Web服务Key } c.JSON(http.StatusOK, gin.H{ "amap_key": config.AmapKey, "amap_js_key": jsKey, "amap_js_secret": config.AmapJsSecret, }) } // geocodeHandler 地理编码 - 地址转坐标 func geocodeHandler(c *gin.Context) { var req struct { Address string `json:"address"` City string `json:"city"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求参数"}) return } if req.Address == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "地址不能为空"}) return } // 调用高德地理编码API apiURL := fmt.Sprintf( "https://restapi.amap.com/v3/geocode/geo?key=%s&address=%s&city=%s", config.AmapKey, url.QueryEscape(req.Address), url.QueryEscape(req.City), ) resp, err := http.Get(apiURL) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "请求高德API失败"}) return } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) var amapResp AmapGeoResponse if err := json.Unmarshal(body, &amapResp); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "解析响应失败"}) return } if amapResp.Status != "1" || len(amapResp.Geocodes) == 0 { c.JSON(http.StatusNotFound, gin.H{"error": "未找到该地址"}) return } // 解析坐标 location := amapResp.Geocodes[0].Location coords := strings.Split(location, ",") if len(coords) != 2 { c.JSON(http.StatusInternalServerError, gin.H{"error": "坐标格式错误"}) return } var lng, lat float64 fmt.Sscanf(coords[0], "%f", &lng) fmt.Sscanf(coords[1], "%f", &lat) c.JSON(http.StatusOK, gin.H{ "location": Location{ Name: req.Address, Address: amapResp.Geocodes[0].FormattedAddress, Coordinate: Coordinate{ Lng: lng, Lat: lat, }, }, }) } // calculateCenterHandler 计算多点中心 func calculateCenterHandler(c *gin.Context) { var req CenterRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求参数"}) return } if len(req.Locations) < 2 { c.JSON(http.StatusBadRequest, gin.H{"error": "至少需要2个位置点"}) return } // 计算几何中心(加权平均) center := calculateGeometricCenter(req.Locations) c.JSON(http.StatusOK, gin.H{ "center": center, }) } // calculateGeometricCenter 计算几何中心点 func calculateGeometricCenter(locations []Location) Coordinate { // 使用球面坐标转换计算更准确的中心点 var x, y, z float64 for _, loc := range locations { // 转换为弧度 latRad := loc.Coordinate.Lat * math.Pi / 180 lngRad := loc.Coordinate.Lng * math.Pi / 180 // 转换为笛卡尔坐标 x += math.Cos(latRad) * math.Cos(lngRad) y += math.Cos(latRad) * math.Sin(lngRad) z += math.Sin(latRad) } // 平均值 n := float64(len(locations)) x /= n y /= n z /= n // 转换回经纬度 lng := math.Atan2(y, x) * 180 / math.Pi hyp := math.Sqrt(x*x + y*y) lat := math.Atan2(z, hyp) * 180 / math.Pi return Coordinate{ Lng: lng, Lat: lat, } } // searchPOIHandler 搜索周边POI func searchPOIHandler(c *gin.Context) { var req SearchRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求参数"}) return } if req.Keywords == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "搜索关键词不能为空"}) return } if req.Radius <= 0 { req.Radius = 3000 // 默认3公里 } // 调用高德POI搜索API location := fmt.Sprintf("%f,%f", req.Center.Lng, req.Center.Lat) apiURL := fmt.Sprintf( "https://restapi.amap.com/v3/place/around?key=%s&location=%s&keywords=%s&radius=%d&offset=20&page=1&extensions=all", config.AmapKey, location, url.QueryEscape(req.Keywords), req.Radius, ) resp, err := http.Get(apiURL) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "请求高德API失败"}) return } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) log.Printf("高德POI搜索响应: %s", string(body)) var amapResp AmapPOIResponse if err := json.Unmarshal(body, &amapResp); err != nil { log.Printf("解析POI响应失败: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "解析响应失败"}) return } if amapResp.Status != "1" { log.Printf("高德POI搜索API返回错误: status=%s, info=%s", amapResp.Status, amapResp.Info) c.JSON(http.StatusInternalServerError, gin.H{"error": "高德API返回错误: " + amapResp.Info}) return } // 转换POI数据 pois := make([]POI, 0, len(amapResp.Pois)) for _, p := range amapResp.Pois { coords := strings.Split(p.Location, ",") if len(coords) != 2 { continue } var lng, lat float64 fmt.Sscanf(coords[0], "%f", &lng) fmt.Sscanf(coords[1], "%f", &lat) // 处理可能是数组或字符串的字段 address, _ := p.Address.(string) tel, _ := p.Tel.(string) pois = append(pois, POI{ ID: p.ID, Name: p.Name, Type: p.Type, Address: address, Location: Coordinate{ Lng: lng, Lat: lat, }, Distance: p.Distance, Tel: tel, }) } c.JSON(http.StatusOK, gin.H{ "pois": pois, "count": len(pois), }) } // getTipsHandler 输入提示 func getTipsHandler(c *gin.Context) { keywords := c.Query("keywords") city := c.Query("city") if keywords == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "关键词不能为空"}) return } apiURL := fmt.Sprintf( "https://restapi.amap.com/v3/assistant/inputtips?key=%s&keywords=%s&city=%s&datatype=all", config.AmapKey, url.QueryEscape(keywords), url.QueryEscape(city), ) resp, err := http.Get(apiURL) if err != nil { log.Printf("请求高德API失败: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "请求高德API失败"}) return } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) var amapResp AmapTipsResponse if err := json.Unmarshal(body, &amapResp); err != nil { log.Printf("解析响应失败: %v, body: %s", err, string(body)) c.JSON(http.StatusInternalServerError, gin.H{"error": "解析响应失败"}) return } if amapResp.Status != "1" { log.Printf("高德API返回错误: status=%s, info=%s", amapResp.Status, amapResp.Info) c.JSON(http.StatusInternalServerError, gin.H{"error": "高德API返回错误: " + amapResp.Info}) return } // 过滤有效的提示 tips := make([]map[string]interface{}, 0) for _, tip := range amapResp.Tips { // 解析 location 字段(可能是字符串或空数组) locationStr, ok := tip.Location.(string) if !ok || locationStr == "" { continue // 跳过非字符串类型的 location } coords := strings.Split(locationStr, ",") if len(coords) != 2 { continue } var lng, lat float64 fmt.Sscanf(coords[0], "%f", &lng) fmt.Sscanf(coords[1], "%f", &lat) // 解析 address 字段(可能是字符串或数组) addressStr, _ := tip.Address.(string) // 解析 id 字段(可能是字符串或数组) idStr, _ := tip.ID.(string) tips = append(tips, map[string]interface{}{ "id": idStr, "name": tip.Name, "district": tip.District, "address": addressStr, "location": Coordinate{Lng: lng, Lat: lat}, }) } c.JSON(http.StatusOK, gin.H{ "tips": tips, }) }