package com.doumee.core.utils.geocode; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.doumee.core.utils.Http; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.io.IOException; import java.net.URLEncoder; /** * 高德地图工具类 * * @Author : Rk * @create 2026/4/20 */ @Slf4j @Component public class MapUtil { private static String amapKey; /** 逆地理解析 */ private static final String GEO_URL = "https://restapi.amap.com/v3/geocode/regeo"; /** 正向地理解析 */ private static final String GEOCODE_URL = "https://restapi.amap.com/v3/geocode/geo"; /** 驾车路径规划 */ private static final String DRIVING_URL = "https://restapi.amap.com/v3/direction/driving"; /** 骑行路径规划 */ private static final String BICYCLING_URL = "https://restapi.amap.com/v4/direction/bicycling"; @Value("${geocode_map_key:}") public void setAmapKey(String amapKey) { MapUtil.amapKey = amapKey; } /** * 正向地理解析 - 根据地址获取经纬度 * * @param address 地址文本(如"四川省成都市") * @return "lat,lng" 格式的经纬度字符串,解析失败返回 null */ public static String geocode(String address) { try { String url = GEOCODE_URL + "?key=" + amapKey + "&address=" + URLEncoder.encode(address, "UTF-8"); log.info("高德地图正向地理解析请求: address={}", address); JSONObject json = new Http().build(url) .setConnectTimeout(5000) .setReadTimeout(10000) .get() .toJSONObject(); log.info("高德地图正向地理解析响应: {}", json); if (!"1".equals(json.getString("status"))) { log.warn("高德地图正向地理解析失败: {}", json.getString("info")); return null; } JSONArray geocodes = json.getJSONArray("geocodes"); if (geocodes == null || geocodes.isEmpty()) { log.warn("高德地图正向地理解析无结果: address={}", address); return null; } String location = geocodes.getJSONObject(0).getString("location"); // lng,lat if (StringUtils.isBlank(location)) { return null; } String[] parts = location.split(","); // 转为 lat,lng 格式 return parts[1] + "," + parts[0]; } catch (Exception e) { log.error("高德地图正向地理解析异常: address={}", address, e); return null; } } /** * 逆地理解析 - 根据经纬度获取地址信息 * 高德坐标系为 lng,lat(与腾讯 lat,lng 相反) * * @param lat 纬度 * @param lng 经度 * @return 包含 ad_info 城市信息的 JSONObject(兼容腾讯返回格式) */ public static JSONObject reverseGeocode(double lat, double lng) { try { String url = GEO_URL + "?key=" + amapKey + "&location=" + lng + "," + lat; log.info("高德地图逆地理解析请求: lat={}, lng={}", lat, lng); JSONObject json = new Http().build(url) .setConnectTimeout(5000) .setReadTimeout(10000) .get() .toJSONObject(); log.info("高德地图逆地理解析响应: {}", json); if (!"1".equals(json.getString("status"))) { throw new RuntimeException("高德地图逆地理解析失败: " + json.getString("info")); } // 转换为兼容腾讯返回格式: result.ad_info.city / adcode / district JSONObject regeocode = json.getJSONObject("regeocode"); JSONObject addressComponent = regeocode.getJSONObject("addressComponent"); JSONObject adInfo = new JSONObject(); adInfo.put("adcode", addressComponent.getString("adcode")); adInfo.put("city", addressComponent.getString("city")); adInfo.put("district", addressComponent.getString("district")); adInfo.put("province", addressComponent.getString("province")); adInfo.put("nation", addressComponent.getString("country")); JSONObject result = new JSONObject(); result.put("ad_info", adInfo); result.put("formatted_addresses", regeocode.getString("formatted_address")); return result; } catch (IOException e) { log.error("高德地图逆地理解析异常", e); throw new RuntimeException("高德地图逆地理解析异常", e); } } /** * 判断两个经纬度是否在同一个城市 */ public static boolean isSameCity(double lat1, double lng1, double lat2, double lng2) { JSONObject result1 = reverseGeocode(lat1, lng1); JSONObject result2 = reverseGeocode(lat2, lng2); String city1 = result1.getJSONObject("ad_info").getString("city"); String city2 = result2.getJSONObject("ad_info").getString("city"); log.info("判断同城: ({},{}) => city={}, ({},{}) => city={}", lat1, lng1, city1, lat2, lng2, city2); return city1 != null && city1.equals(city2); } /** * 路径规划(统一入口) * 内部根据 mode 调用高德不同的路径规划接口 * * @param mode 模式:driving(驾车)、bicycling(骑行) * @param from 起点,格式:lat,lng * @param to 终点,格式:lat,lng * @return JSONObject 包含 distance(米) 和 duration(秒) */ public static JSONObject direction(String mode, String from, String to) { // 高德坐标系为 lng,lat String[] fromArr = from.split(","); String[] toArr = to.split(","); String origin = fromArr[1] + "," + fromArr[0]; // lng,lat String destination = toArr[1] + "," + toArr[0]; // lng,lat try { String url; if ("bicycling".equals(mode)) { url = BICYCLING_URL + "?key=" + amapKey + "&origin=" + URLEncoder.encode(origin, "UTF-8") + "&destination=" + URLEncoder.encode(destination, "UTF-8"); } else { // 默认驾车 url = DRIVING_URL + "?key=" + amapKey + "&origin=" + URLEncoder.encode(origin, "UTF-8") + "&destination=" + URLEncoder.encode(destination, "UTF-8"); } log.info("高德地图路径规划请求: mode={}, from={}, to={}", mode, from, to); JSONObject json = new Http().build(url) .setConnectTimeout(5000) .setReadTimeout(10000) .get() .toJSONObject(); log.info("高德地图路径规划响应: {}", json); if (!"1".equals(json.getString("status"))) { throw new RuntimeException("高德地图路径规划失败: " + json.getString("info")); } // 提取第一条路径的 distance 和 duration JSONObject routeData; if ("bicycling".equals(mode)) { routeData = json.getJSONObject("data"); } else { routeData = json.getJSONObject("route"); } JSONObject path = routeData.getJSONArray("paths").getJSONObject(0); long distance = path.getLongValue("distance"); long duration = path.getLongValue("duration"); JSONObject result = new JSONObject(); result.put("distance", distance); result.put("duration", duration); return result; } catch (IOException e) { log.error("高德地图路径规划异常", e); throw new RuntimeException("高德地图路径规划异常", e); } } public static JSONObject directionInfo(String mode, String from, String to) { // 高德坐标系为 lng,lat String[] fromArr = from.split(","); String[] toArr = to.split(","); String origin = fromArr[1] + "," + fromArr[0]; // lng,lat String destination = toArr[1] + "," + toArr[0]; // lng,lat try { String url; if ("bicycling".equals(mode)) { url = BICYCLING_URL + "?key=" + amapKey + "&origin=" + URLEncoder.encode(origin, "UTF-8") + "&destination=" + URLEncoder.encode(destination, "UTF-8"); } else { // 默认驾车 url = DRIVING_URL + "?key=" + amapKey + "&origin=" + URLEncoder.encode(origin, "UTF-8") + "&destination=" + URLEncoder.encode(destination, "UTF-8"); } log.info("高德地图路径规划请求: mode={}, from={}, to={}", mode, from, to); JSONObject json = new Http().build(url) .setConnectTimeout(5000) .setReadTimeout(10000) .get() .toJSONObject(); log.info("高德地图路径规划响应: {}", json); return json; } catch (IOException e) { log.error("高德地图路径规划异常", e); throw new RuntimeException("高德地图路径规划异常", e); } } }