GoogleRoutesDistanceProvider.java 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. package com.ruoyi.app.flashdelivery.route;
  2. import com.alibaba.fastjson2.JSON;
  3. import com.alibaba.fastjson2.JSONArray;
  4. import com.alibaba.fastjson2.JSONObject;
  5. import com.ruoyi.common.core.domain.entity.SysDictData;
  6. import com.ruoyi.common.utils.DictUtils;
  7. import com.ruoyi.system.domain.flash.FlashDeliveryVehicleType;
  8. import org.apache.http.client.config.RequestConfig;
  9. import org.apache.http.client.methods.CloseableHttpResponse;
  10. import org.apache.http.client.methods.HttpPost;
  11. import org.apache.http.entity.ContentType;
  12. import org.apache.http.entity.StringEntity;
  13. import org.apache.http.impl.client.CloseableHttpClient;
  14. import org.apache.http.impl.client.HttpClients;
  15. import org.apache.http.util.EntityUtils;
  16. import org.springframework.stereotype.Component;
  17. import java.math.BigDecimal;
  18. import java.nio.charset.StandardCharsets;
  19. import java.util.List;
  20. /** Google Routes API 的服务端实现,复用既有地图字典中的 Key。 */
  21. @Component
  22. public class GoogleRoutesDistanceProvider implements RouteDistanceProvider {
  23. private static final String ENDPOINT = "https://routes.googleapis.com/directions/v2:computeRoutes";
  24. private static final RequestConfig TIMEOUTS = RequestConfig.custom()
  25. .setConnectTimeout(2500).setConnectionRequestTimeout(2500).setSocketTimeout(3500).build();
  26. /** 调用 Google Routes API 获取所选车型路线距离与时长;密钥缺失或响应异常时抛错,由上层降级。 */
  27. @Override
  28. public RouteDistance route(BigDecimal originLatitude, BigDecimal originLongitude,
  29. BigDecimal destinationLatitude, BigDecimal destinationLongitude,
  30. Integer vehicleType) {
  31. return route(List.of(new GeoPoint(originLatitude, originLongitude),
  32. new GeoPoint(destinationLatitude, destinationLongitude)), vehicleType);
  33. }
  34. /** 一次请求完整路线,按原始顺序传中途点;不调用路线优化接口。 */
  35. @Override
  36. public RouteDistance route(List<GeoPoint> points, Integer vehicleType) {
  37. JSONObject body = requestBody(points, vehicleType);
  38. // 闪送与外卖共用既有地图 Key,避免维护第二套字典配置。
  39. List<SysDictData> keys = DictUtils.getDictCache(mapKeyDictType());
  40. if (keys == null || keys.isEmpty() || keys.get(0).getDictValue() == null
  41. || keys.get(0).getDictValue().isBlank()) {
  42. throw new IllegalStateException("google routes key unavailable");
  43. }
  44. HttpPost request = new HttpPost(ENDPOINT);
  45. request.setConfig(TIMEOUTS);
  46. request.setHeader("X-Goog-Api-Key", keys.get(0).getDictValue());
  47. // 只请求计价需要的字段,减少响应体及不必要的数据暴露。
  48. request.setHeader("X-Goog-FieldMask", "routes.distanceMeters,routes.duration");
  49. request.setEntity(new StringEntity(body.toJSONString(), ContentType.APPLICATION_JSON));
  50. try (CloseableHttpClient client = HttpClients.custom().disableAutomaticRetries().build();
  51. CloseableHttpResponse response = client.execute(request)) {
  52. int statusCode = response.getStatusLine().getStatusCode();
  53. if (statusCode < 200 || statusCode >= 300) {
  54. // 仅保留 HTTP 状态,绝不记录 Key、地址或完整 Google 响应内容。
  55. throw new IllegalStateException("google routes http status " + statusCode);
  56. }
  57. JSONObject json = JSON.parseObject(EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8));
  58. JSONArray routes = json.getJSONArray("routes");
  59. if (routes == null || routes.isEmpty()) {
  60. throw new IllegalStateException("google routes empty response");
  61. }
  62. JSONObject first = routes.getJSONObject(0);
  63. int meters = first.getIntValue("distanceMeters");
  64. if (meters <= 0) {
  65. throw new IllegalStateException("google routes invalid distance");
  66. }
  67. return new RouteDistance(meters, parseDuration(first.getString("duration")), "ROUTE");
  68. } catch (IllegalStateException exception) {
  69. throw exception;
  70. } catch (Exception exception) {
  71. throw new IllegalStateException("google routes request failed", exception);
  72. }
  73. }
  74. /** 两点和多点共用的地图请求体,保留重复坐标对应的独立站点。 */
  75. JSONObject requestBody(List<GeoPoint> points, Integer vehicleType) {
  76. FlashDeliveryRouteService.validatePoints(points);
  77. JSONObject body = new JSONObject();
  78. GeoPoint origin = points.get(0);
  79. GeoPoint destination = points.get(points.size() - 1);
  80. body.put("origin", waypoint(origin.latitude(), origin.longitude()));
  81. body.put("destination", waypoint(destination.latitude(), destination.longitude()));
  82. if (points.size() > 2) {
  83. JSONArray intermediates = new JSONArray();
  84. for (GeoPoint point : points.subList(1, points.size() - 1)) {
  85. intermediates.add(waypoint(point.latitude(), point.longitude()));
  86. }
  87. body.put("intermediates", intermediates);
  88. }
  89. body.put("optimizeWaypointOrder", false);
  90. body.put("travelMode", travelModeFor(vehicleType));
  91. body.put("routingPreference", "TRAFFIC_AWARE");
  92. body.put("computeAlternativeRoutes", false);
  93. body.put("units", "METRIC");
  94. return body;
  95. }
  96. /** 保留测试和旧调用的默认机车车型。 */
  97. JSONObject requestBody(List<GeoPoint> points) {
  98. return requestBody(points, FlashDeliveryVehicleType.MOTORCYCLE);
  99. }
  100. private JSONObject waypoint(BigDecimal latitude, BigDecimal longitude) {
  101. JSONObject latLng = new JSONObject();
  102. latLng.put("latitude", latitude);
  103. latLng.put("longitude", longitude);
  104. JSONObject location = new JSONObject();
  105. location.put("latLng", latLng);
  106. JSONObject waypoint = new JSONObject();
  107. waypoint.put("location", location);
  108. return waypoint;
  109. }
  110. /** Google TWO_WHEELER 对应机车;无效或历史空值安全按机车回落。 */
  111. static String travelModeFor(Integer vehicleType) {
  112. return Integer.valueOf(FlashDeliveryVehicleType.CAR).equals(vehicleType)
  113. ? "DRIVE" : "TWO_WHEELER";
  114. }
  115. /** 闪送与外卖共用的地图 Key 字典类型。 */
  116. static String mapKeyDictType() {
  117. return "sys_googlemap_key";
  118. }
  119. private Integer parseDuration(String value) {
  120. if (value == null || !value.endsWith("s")) return null;
  121. try {
  122. return new BigDecimal(value.substring(0, value.length() - 1)).intValue();
  123. } catch (NumberFormatException exception) {
  124. return null;
  125. }
  126. }
  127. }