package com.ruoyi.app.flashdelivery.route; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONObject; import com.ruoyi.common.core.domain.entity.SysDictData; import com.ruoyi.common.utils.DictUtils; import com.ruoyi.system.domain.flash.FlashDeliveryVehicleType; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; import org.springframework.stereotype.Component; import java.math.BigDecimal; import java.nio.charset.StandardCharsets; import java.util.List; /** Google Routes API 的服务端实现,复用既有地图字典中的 Key。 */ @Component public class GoogleRoutesDistanceProvider implements RouteDistanceProvider { private static final String ENDPOINT = "https://routes.googleapis.com/directions/v2:computeRoutes"; private static final RequestConfig TIMEOUTS = RequestConfig.custom() .setConnectTimeout(2500).setConnectionRequestTimeout(2500).setSocketTimeout(3500).build(); /** 调用 Google Routes API 获取所选车型路线距离与时长;密钥缺失或响应异常时抛错,由上层降级。 */ @Override public RouteDistance route(BigDecimal originLatitude, BigDecimal originLongitude, BigDecimal destinationLatitude, BigDecimal destinationLongitude, Integer vehicleType) { return route(List.of(new GeoPoint(originLatitude, originLongitude), new GeoPoint(destinationLatitude, destinationLongitude)), vehicleType); } /** 一次请求完整路线,按原始顺序传中途点;不调用路线优化接口。 */ @Override public RouteDistance route(List points, Integer vehicleType) { JSONObject body = requestBody(points, vehicleType); // 闪送与外卖共用既有地图 Key,避免维护第二套字典配置。 List keys = DictUtils.getDictCache(mapKeyDictType()); if (keys == null || keys.isEmpty() || keys.get(0).getDictValue() == null || keys.get(0).getDictValue().isBlank()) { throw new IllegalStateException("google routes key unavailable"); } HttpPost request = new HttpPost(ENDPOINT); request.setConfig(TIMEOUTS); request.setHeader("X-Goog-Api-Key", keys.get(0).getDictValue()); // 只请求计价需要的字段,减少响应体及不必要的数据暴露。 request.setHeader("X-Goog-FieldMask", "routes.distanceMeters,routes.duration"); request.setEntity(new StringEntity(body.toJSONString(), ContentType.APPLICATION_JSON)); try (CloseableHttpClient client = HttpClients.custom().disableAutomaticRetries().build(); CloseableHttpResponse response = client.execute(request)) { int statusCode = response.getStatusLine().getStatusCode(); if (statusCode < 200 || statusCode >= 300) { // 仅保留 HTTP 状态,绝不记录 Key、地址或完整 Google 响应内容。 throw new IllegalStateException("google routes http status " + statusCode); } JSONObject json = JSON.parseObject(EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8)); JSONArray routes = json.getJSONArray("routes"); if (routes == null || routes.isEmpty()) { throw new IllegalStateException("google routes empty response"); } JSONObject first = routes.getJSONObject(0); int meters = first.getIntValue("distanceMeters"); if (meters <= 0) { throw new IllegalStateException("google routes invalid distance"); } return new RouteDistance(meters, parseDuration(first.getString("duration")), "ROUTE"); } catch (IllegalStateException exception) { throw exception; } catch (Exception exception) { throw new IllegalStateException("google routes request failed", exception); } } /** 两点和多点共用的地图请求体,保留重复坐标对应的独立站点。 */ JSONObject requestBody(List points, Integer vehicleType) { FlashDeliveryRouteService.validatePoints(points); JSONObject body = new JSONObject(); GeoPoint origin = points.get(0); GeoPoint destination = points.get(points.size() - 1); body.put("origin", waypoint(origin.latitude(), origin.longitude())); body.put("destination", waypoint(destination.latitude(), destination.longitude())); if (points.size() > 2) { JSONArray intermediates = new JSONArray(); for (GeoPoint point : points.subList(1, points.size() - 1)) { intermediates.add(waypoint(point.latitude(), point.longitude())); } body.put("intermediates", intermediates); } body.put("optimizeWaypointOrder", false); body.put("travelMode", travelModeFor(vehicleType)); body.put("routingPreference", "TRAFFIC_AWARE"); body.put("computeAlternativeRoutes", false); body.put("units", "METRIC"); return body; } /** 保留测试和旧调用的默认机车车型。 */ JSONObject requestBody(List points) { return requestBody(points, FlashDeliveryVehicleType.MOTORCYCLE); } private JSONObject waypoint(BigDecimal latitude, BigDecimal longitude) { JSONObject latLng = new JSONObject(); latLng.put("latitude", latitude); latLng.put("longitude", longitude); JSONObject location = new JSONObject(); location.put("latLng", latLng); JSONObject waypoint = new JSONObject(); waypoint.put("location", location); return waypoint; } /** Google TWO_WHEELER 对应机车;无效或历史空值安全按机车回落。 */ static String travelModeFor(Integer vehicleType) { return Integer.valueOf(FlashDeliveryVehicleType.CAR).equals(vehicleType) ? "DRIVE" : "TWO_WHEELER"; } /** 闪送与外卖共用的地图 Key 字典类型。 */ static String mapKeyDictType() { return "sys_googlemap_key"; } private Integer parseDuration(String value) { if (value == null || !value.endsWith("s")) return null; try { return new BigDecimal(value.substring(0, value.length() - 1)).intValue(); } catch (NumberFormatException exception) { return null; } } }