AmapRouteClient.java 5.48 KB
package com.diligrp.rider.service.amap;

import com.diligrp.rider.config.AmapProperties;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Optional;

/**
 * 高德电动车路径规划客户端(/v5/direction/electrobike)。
 *
 * 任何失败场景(未启用 / Key 缺失 / 坐标非法 / 起止同点 / HTTP 异常 / status != "1" / paths 空 / 字段缺失)
 * 均返回 Optional.empty(),由调用方决定降级策略。
 */
@Slf4j
@Component
public class AmapRouteClient {

    private final AmapProperties amapProperties;
    private final ObjectMapper objectMapper;
    private final HttpClient httpClient;

    public AmapRouteClient(AmapProperties amapProperties, ObjectMapper objectMapper) {
        this.amapProperties = amapProperties;
        this.objectMapper = objectMapper;
        this.httpClient = HttpClient.newBuilder()
                .connectTimeout(Duration.ofMillis(amapProperties.getConnectTimeoutMillis()))
                .build();
    }

    /** 高德电动车路径规划结果:原始距离(米)与耗时(秒)。 */
    public record AmapRoute(int distanceMeters, int durationSeconds) {}

    /**
     * 调用高德电动车路径规划,返回原始距离(米)+ 耗时(秒)。
     */
    public Optional<AmapRoute> getElectrobikeRoute(String startLng, String startLat,
                                                  String endLng, String endLat) {
        if (!amapProperties.isEnabled()) {
            return Optional.empty();
        }
        if (amapProperties.getKey() == null || amapProperties.getKey().isBlank()) {
            log.warn("Amap key 未配置,跳过高德调用");
            return Optional.empty();
        }

        double sLng;
        double sLat;
        double eLng;
        double eLat;
        try {
            sLng = Double.parseDouble(startLng);
            sLat = Double.parseDouble(startLat);
            eLng = Double.parseDouble(endLng);
            eLat = Double.parseDouble(endLat);
        } catch (Exception e) {
            return Optional.empty();
        }
        if (sLng == eLng && sLat == eLat) {
            return Optional.empty();
        }

        String origin = String.format("%.6f,%.6f", sLng, sLat);
        String destination = String.format("%.6f,%.6f", eLng, eLat);
        String url = amapProperties.getBaseUrl() + "/v5/direction/electrobike"
                + "?key=" + URLEncoder.encode(amapProperties.getKey(), StandardCharsets.UTF_8)
                + "&origin=" + URLEncoder.encode(origin, StandardCharsets.UTF_8)
                + "&destination=" + URLEncoder.encode(destination, StandardCharsets.UTF_8)
                + "&show_fields=cost";

        try {
            HttpRequest request = HttpRequest.newBuilder()
                    .uri(URI.create(url))
                    .timeout(Duration.ofMillis(amapProperties.getReadTimeoutMillis()))
                    .GET()
                    .build();
            HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
            if (response.statusCode() != 200) {
                log.warn("Amap electrobike HTTP {} body={}", response.statusCode(), trim(response.body()));
                return Optional.empty();
            }
            JsonNode root = objectMapper.readTree(response.body());
            String status = root.path("status").asText();
            if (!"1".equals(status)) {
                log.warn("Amap electrobike status={} info={} infocode={}",
                        status, root.path("info").asText(), root.path("infocode").asText());
                return Optional.empty();
            }
            JsonNode paths = root.path("route").path("paths");
            if (!paths.isArray() || paths.isEmpty()) {
                log.warn("Amap electrobike paths 空 body={}", trim(response.body()));
                return Optional.empty();
            }
            JsonNode first = paths.get(0);
            long distanceMeters = first.path("distance").asLong(-1);
            long durationSeconds = first.path("cost").path("duration").asLong(-1);
            if (distanceMeters < 0 || durationSeconds <= 0) {
                log.warn("Amap electrobike 字段异常 distance={} duration={} body={}",
                        distanceMeters, durationSeconds, trim(response.body()));
                return Optional.empty();
            }
            return Optional.of(new AmapRoute((int) distanceMeters, (int) durationSeconds));
        } catch (Exception e) {
            log.warn("Amap electrobike 调用失败 err={}", e.getMessage());
            return Optional.empty();
        }
    }

    /**
     * 返回预计骑行分钟数(向上取整)。委托 {@link #getElectrobikeRoute}。
     */
    public Optional<Integer> getElectrobikeDurationMinutes(String startLng, String startLat,
                                                          String endLng, String endLat) {
        return getElectrobikeRoute(startLng, startLat, endLng, endLat)
                .map(r -> (int) Math.ceil(r.durationSeconds() / 60.0));
    }

    private String trim(String body) {
        if (body == null) return "";
        return body.length() > 300 ? body.substring(0, 300) : body;
    }
}