package com.doumee.core.utils.tencent; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.doumee.core.utils.Http; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.io.IOException; import java.net.URLEncoder; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; /** * 腾讯地图工具类 * * @Author : Rk * @create 2026/4/14 15:58 */ @Slf4j //@Component public class MapUtil { private static String tencentKey; /** 距离矩阵API */ public static final String MATRIX_URL = "https://apis.map.qq.com/ws/distance/v1/matrix"; /** 逆地理解析 */ public static final String GEO_URL = "https://apis.map.qq.com/ws/geocoder/v1/"; /** 支持的模式 */ private static final List SUPPORTED_MODES = Arrays.asList("driving", "bicycling"); @Value("${tencent_key}") public void setTencentKey(String tencentKey) { MapUtil.tencentKey = tencentKey; } /** * 批量距离矩阵计算 * * @param mode 模式:driving(驾车)、bicycling(自行车) * @param fromPoints 起点列表,格式:lat,lng(最多10个) * @param toPoints 终点列表,格式:lat,lng(最多20个) * @return result.rows 矩阵数据,每个元素包含 distance(米) 和 duration(秒) */ public static JSONObject distanceMatrix(String mode, List fromPoints, List toPoints) { if (!SUPPORTED_MODES.contains(mode)) { throw new IllegalArgumentException("不支持的模式: " + mode + ",仅支持: " + SUPPORTED_MODES); } if (fromPoints == null || fromPoints.isEmpty() || toPoints == null || toPoints.isEmpty()) { throw new IllegalArgumentException("起点和终点列表不能为空"); } String from = String.join(";", fromPoints); String to = String.join(";", toPoints); try { String url = MATRIX_URL + "?key=" + tencentKey + "&mode=" + mode + "&from=" + URLEncoder.encode(from, "UTF-8") + "&to=" + URLEncoder.encode(to, "UTF-8"); log.info("腾讯地图矩阵API请求: mode={}, from={}, to={}", mode, from, to); JSONObject json = new Http().build(url) .setConnectTimeout(5000) .setReadTimeout(10000) .get() .toJSONObject(); log.info("腾讯地图矩阵API响应: {}", json); if (json.getIntValue("status") != 0) { throw new RuntimeException("腾讯地图矩阵API调用失败: " + json.getString("message")); } return json.getJSONObject("result"); } catch (IOException e) { log.error("腾讯地图矩阵API调用异常", e); throw new RuntimeException("腾讯地图矩阵API调用异常", e); } } /** * 单对距离计算(便捷方法) * * @param mode 模式:driving(驾车)、bicycling(自行车) * @param from 起点格式:lat,lng * @param to 终点格式:lat,lng * @return 第一个元素的 distance(米) 和 duration(秒) */ public static JSONObject distanceSingle(String mode, String from, String to) { JSONObject result = distanceMatrix(mode, Arrays.asList(from), Arrays.asList(to)); JSONArray rows = result.getJSONArray("rows"); if (rows != null && !rows.isEmpty()) { JSONArray elements = rows.getJSONObject(0).getJSONArray("elements"); if (elements != null && !elements.isEmpty()) { return elements.getJSONObject(0); } } return new JSONObject(); } /** * 多起点到单终点(便捷方法) * 返回每个起点到终点的距离和耗时,顺序与fromPoints对应 * * @param mode 模式 * @param fromPoints 起点列表 * @param to 单个终点 * @return 距离耗时列表,顺序与fromPoints对应 */ public static List distanceToOne(String mode, List fromPoints, String to) { JSONObject result = distanceMatrix(mode, fromPoints, Arrays.asList(to)); JSONArray rows = result.getJSONArray("rows"); return rows == null ? Arrays.asList() : rows.stream() .map(row -> ((JSONObject) row).getJSONArray("elements").getJSONObject(0)) .collect(Collectors.toList()); } /** * 逆地理解析 - 根据经纬度获取地址信息 * * @param lat 纬度 * @param lng 经度 * @return result.ad_info 中的 adcode(区划码)、city(城市)、district(区) 等信息 */ public static JSONObject reverseGeocode(double lat, double lng) { try { String url = GEO_URL + "?key=" + tencentKey + "&location=" + lat + "," + lng; log.info("腾讯地图逆地理解析请求: location={},{}", lat, lng); JSONObject json = new Http().build(url) .setConnectTimeout(5000) .setReadTimeout(10000) .get() .toJSONObject(); log.info("腾讯地图逆地理解析响应: {}", json); if (json.getIntValue("status") != 0) { throw new RuntimeException("腾讯地图逆地理解析失败: " + json.getString("message")); } return json.getJSONObject("result"); } catch (IOException e) { log.error("腾讯地图逆地理解析异常", e); throw new RuntimeException("腾讯地图逆地理解析异常", e); } } /** * 判断两个经纬度是否在同一个城市 * * @param lat1 第一个点纬度 * @param lng1 第一个点经度 * @param lat2 第二个点纬度 * @param lng2 第二个点经度 * @return true=同城,false=不同城 */ 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); } }