Commit 815273bb20af37aa0e8c8bc10deed38d29a23254
1 parent
f3ec2e0c
新增高德电动车路径规划支持,完善配送费计算与路线规划接口
Showing
9 changed files
with
391 additions
and
3 deletions
src/main/java/com/diligrp/rider/config/AmapProperties.java
0 → 100644
| 1 | +package com.diligrp.rider.config; | |
| 2 | + | |
| 3 | +import lombok.Data; | |
| 4 | +import org.springframework.boot.context.properties.ConfigurationProperties; | |
| 5 | +import org.springframework.context.annotation.Configuration; | |
| 6 | + | |
| 7 | +/** | |
| 8 | + * 高德 Web 服务 API 配置(电动车路径规划)。 | |
| 9 | + */ | |
| 10 | +@Data | |
| 11 | +@Configuration | |
| 12 | +@ConfigurationProperties(prefix = "amap") | |
| 13 | +public class AmapProperties { | |
| 14 | + | |
| 15 | + /** 是否启用高德 ETA;false 时调用直接返回空,由上游降级到直线距离公式 */ | |
| 16 | + private boolean enabled = true; | |
| 17 | + | |
| 18 | + /** 高德 Web 服务 API Key */ | |
| 19 | + private String key; | |
| 20 | + | |
| 21 | + /** 高德 Web 服务基础 URL */ | |
| 22 | + private String baseUrl = "https://restapi.amap.com"; | |
| 23 | + | |
| 24 | + /** HTTP 连接超时(毫秒) */ | |
| 25 | + private int connectTimeoutMillis = 3000; | |
| 26 | + | |
| 27 | + /** HTTP 读取超时(毫秒) */ | |
| 28 | + private int readTimeoutMillis = 3000; | |
| 29 | +} | ... | ... |
src/main/java/com/diligrp/rider/controller/OpenApiController.java
| ... | ... | @@ -4,15 +4,21 @@ import com.diligrp.rider.common.exception.BizException; |
| 4 | 4 | import com.diligrp.rider.common.result.Result; |
| 5 | 5 | import com.diligrp.rider.dto.DeliveryFeeCalcDTO; |
| 6 | 6 | import com.diligrp.rider.dto.OpenDeliveryFeeCalcDTO; |
| 7 | +import com.diligrp.rider.dto.OpenRouteCalcDTO; | |
| 7 | 8 | import com.diligrp.rider.entity.OpenApp; |
| 8 | 9 | import com.diligrp.rider.service.DeliveryFeeService; |
| 9 | 10 | import com.diligrp.rider.service.OpenAppService; |
| 11 | +import com.diligrp.rider.service.amap.AmapRouteClient; | |
| 12 | +import com.diligrp.rider.util.GeoUtil; | |
| 10 | 13 | import com.diligrp.rider.vo.DeliveryFeeResultVO; |
| 14 | +import com.diligrp.rider.vo.RouteCalcVO; | |
| 11 | 15 | import jakarta.servlet.http.HttpServletRequest; |
| 12 | 16 | import jakarta.validation.Valid; |
| 13 | 17 | import lombok.RequiredArgsConstructor; |
| 14 | 18 | import org.springframework.web.bind.annotation.*; |
| 15 | 19 | |
| 20 | +import java.util.Optional; | |
| 21 | + | |
| 16 | 22 | /** |
| 17 | 23 | * 开放平台对外接口 |
| 18 | 24 | * 路径前缀 /api/open/** 受 OpenApiInterceptor 签名鉴权保护 |
| ... | ... | @@ -25,6 +31,7 @@ public class OpenApiController { |
| 25 | 31 | |
| 26 | 32 | private final DeliveryFeeService deliveryFeeService; |
| 27 | 33 | private final OpenAppService openAppService; |
| 34 | + private final AmapRouteClient amapRouteClient; | |
| 28 | 35 | |
| 29 | 36 | /** |
| 30 | 37 | * 计算配送费(对外开放版) |
| ... | ... | @@ -55,6 +62,32 @@ public class OpenApiController { |
| 55 | 62 | return Result.success(deliveryFeeService.isServiceEnabled(resolveCityId(request), orderType)); |
| 56 | 63 | } |
| 57 | 64 | |
| 65 | + /** | |
| 66 | + * 路线计算:传入起止经纬度,返回距离(米)与导航耗时(秒)。 | |
| 67 | + * - distance 始终有值:优先高德电动车路径距离,失败时回退到直线距离 | |
| 68 | + * - duration 高德未命中时为 null | |
| 69 | + */ | |
| 70 | + @PostMapping("/route/calc") | |
| 71 | + public Result<RouteCalcVO> routeCalc(@Valid @RequestBody OpenRouteCalcDTO dto) { | |
| 72 | + Optional<AmapRouteClient.AmapRoute> route = amapRouteClient.getElectrobikeRoute( | |
| 73 | + dto.getStartLng(), dto.getStartLat(), dto.getEndLng(), dto.getEndLat()); | |
| 74 | + if (route.isPresent()) { | |
| 75 | + return Result.success(new RouteCalcVO(route.get().distanceMeters(), route.get().durationSeconds())); | |
| 76 | + } | |
| 77 | + double km; | |
| 78 | + try { | |
| 79 | + km = GeoUtil.calcDistanceKm( | |
| 80 | + Double.parseDouble(dto.getStartLat()), | |
| 81 | + Double.parseDouble(dto.getStartLng()), | |
| 82 | + Double.parseDouble(dto.getEndLat()), | |
| 83 | + Double.parseDouble(dto.getEndLng())); | |
| 84 | + } catch (Exception e) { | |
| 85 | + throw new BizException("起止经纬度格式错误"); | |
| 86 | + } | |
| 87 | + int distanceMeters = (int) Math.round(km * 1000); | |
| 88 | + return Result.success(new RouteCalcVO(distanceMeters, null)); | |
| 89 | + } | |
| 90 | + | |
| 58 | 91 | private Long resolveCityId(HttpServletRequest request) { |
| 59 | 92 | String appKey = request.getHeader("X-App-Key"); |
| 60 | 93 | OpenApp app = openAppService.getByAppKey(appKey); | ... | ... |
src/main/java/com/diligrp/rider/dto/OpenRouteCalcDTO.java
0 → 100644
| 1 | +package com.diligrp.rider.dto; | |
| 2 | + | |
| 3 | +import jakarta.validation.constraints.NotNull; | |
| 4 | +import lombok.Data; | |
| 5 | + | |
| 6 | +/** | |
| 7 | + * 开放平台路线计算请求 DTO。 | |
| 8 | + */ | |
| 9 | +@Data | |
| 10 | +public class OpenRouteCalcDTO { | |
| 11 | + | |
| 12 | + /** 起点经度 */ | |
| 13 | + @NotNull(message = "起点经度不能为空") | |
| 14 | + private String startLng; | |
| 15 | + | |
| 16 | + /** 起点纬度 */ | |
| 17 | + @NotNull(message = "起点纬度不能为空") | |
| 18 | + private String startLat; | |
| 19 | + | |
| 20 | + /** 终点经度 */ | |
| 21 | + @NotNull(message = "终点经度不能为空") | |
| 22 | + private String endLng; | |
| 23 | + | |
| 24 | + /** 终点纬度 */ | |
| 25 | + @NotNull(message = "终点纬度不能为空") | |
| 26 | + private String endLat; | |
| 27 | +} | ... | ... |
src/main/java/com/diligrp/rider/service/amap/AmapRouteClient.java
0 → 100644
| 1 | +package com.diligrp.rider.service.amap; | |
| 2 | + | |
| 3 | +import com.diligrp.rider.config.AmapProperties; | |
| 4 | +import com.fasterxml.jackson.databind.JsonNode; | |
| 5 | +import com.fasterxml.jackson.databind.ObjectMapper; | |
| 6 | +import lombok.extern.slf4j.Slf4j; | |
| 7 | +import org.springframework.stereotype.Component; | |
| 8 | + | |
| 9 | +import java.net.URI; | |
| 10 | +import java.net.URLEncoder; | |
| 11 | +import java.net.http.HttpClient; | |
| 12 | +import java.net.http.HttpRequest; | |
| 13 | +import java.net.http.HttpResponse; | |
| 14 | +import java.nio.charset.StandardCharsets; | |
| 15 | +import java.time.Duration; | |
| 16 | +import java.util.Optional; | |
| 17 | + | |
| 18 | +/** | |
| 19 | + * 高德电动车路径规划客户端(/v5/direction/electrobike)。 | |
| 20 | + * | |
| 21 | + * 任何失败场景(未启用 / Key 缺失 / 坐标非法 / 起止同点 / HTTP 异常 / status != "1" / paths 空 / 字段缺失) | |
| 22 | + * 均返回 Optional.empty(),由调用方决定降级策略。 | |
| 23 | + */ | |
| 24 | +@Slf4j | |
| 25 | +@Component | |
| 26 | +public class AmapRouteClient { | |
| 27 | + | |
| 28 | + private final AmapProperties amapProperties; | |
| 29 | + private final ObjectMapper objectMapper; | |
| 30 | + private final HttpClient httpClient; | |
| 31 | + | |
| 32 | + public AmapRouteClient(AmapProperties amapProperties, ObjectMapper objectMapper) { | |
| 33 | + this.amapProperties = amapProperties; | |
| 34 | + this.objectMapper = objectMapper; | |
| 35 | + this.httpClient = HttpClient.newBuilder() | |
| 36 | + .connectTimeout(Duration.ofMillis(amapProperties.getConnectTimeoutMillis())) | |
| 37 | + .build(); | |
| 38 | + } | |
| 39 | + | |
| 40 | + /** 高德电动车路径规划结果:原始距离(米)与耗时(秒)。 */ | |
| 41 | + public record AmapRoute(int distanceMeters, int durationSeconds) {} | |
| 42 | + | |
| 43 | + /** | |
| 44 | + * 调用高德电动车路径规划,返回原始距离(米)+ 耗时(秒)。 | |
| 45 | + */ | |
| 46 | + public Optional<AmapRoute> getElectrobikeRoute(String startLng, String startLat, | |
| 47 | + String endLng, String endLat) { | |
| 48 | + if (!amapProperties.isEnabled()) { | |
| 49 | + return Optional.empty(); | |
| 50 | + } | |
| 51 | + if (amapProperties.getKey() == null || amapProperties.getKey().isBlank()) { | |
| 52 | + log.warn("Amap key 未配置,跳过高德调用"); | |
| 53 | + return Optional.empty(); | |
| 54 | + } | |
| 55 | + | |
| 56 | + double sLng; | |
| 57 | + double sLat; | |
| 58 | + double eLng; | |
| 59 | + double eLat; | |
| 60 | + try { | |
| 61 | + sLng = Double.parseDouble(startLng); | |
| 62 | + sLat = Double.parseDouble(startLat); | |
| 63 | + eLng = Double.parseDouble(endLng); | |
| 64 | + eLat = Double.parseDouble(endLat); | |
| 65 | + } catch (Exception e) { | |
| 66 | + return Optional.empty(); | |
| 67 | + } | |
| 68 | + if (sLng == eLng && sLat == eLat) { | |
| 69 | + return Optional.empty(); | |
| 70 | + } | |
| 71 | + | |
| 72 | + String origin = String.format("%.6f,%.6f", sLng, sLat); | |
| 73 | + String destination = String.format("%.6f,%.6f", eLng, eLat); | |
| 74 | + String url = amapProperties.getBaseUrl() + "/v5/direction/electrobike" | |
| 75 | + + "?key=" + URLEncoder.encode(amapProperties.getKey(), StandardCharsets.UTF_8) | |
| 76 | + + "&origin=" + URLEncoder.encode(origin, StandardCharsets.UTF_8) | |
| 77 | + + "&destination=" + URLEncoder.encode(destination, StandardCharsets.UTF_8) | |
| 78 | + + "&show_fields=cost"; | |
| 79 | + | |
| 80 | + try { | |
| 81 | + HttpRequest request = HttpRequest.newBuilder() | |
| 82 | + .uri(URI.create(url)) | |
| 83 | + .timeout(Duration.ofMillis(amapProperties.getReadTimeoutMillis())) | |
| 84 | + .GET() | |
| 85 | + .build(); | |
| 86 | + HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); | |
| 87 | + if (response.statusCode() != 200) { | |
| 88 | + log.warn("Amap electrobike HTTP {} body={}", response.statusCode(), trim(response.body())); | |
| 89 | + return Optional.empty(); | |
| 90 | + } | |
| 91 | + JsonNode root = objectMapper.readTree(response.body()); | |
| 92 | + String status = root.path("status").asText(); | |
| 93 | + if (!"1".equals(status)) { | |
| 94 | + log.warn("Amap electrobike status={} info={} infocode={}", | |
| 95 | + status, root.path("info").asText(), root.path("infocode").asText()); | |
| 96 | + return Optional.empty(); | |
| 97 | + } | |
| 98 | + JsonNode paths = root.path("route").path("paths"); | |
| 99 | + if (!paths.isArray() || paths.isEmpty()) { | |
| 100 | + log.warn("Amap electrobike paths 空 body={}", trim(response.body())); | |
| 101 | + return Optional.empty(); | |
| 102 | + } | |
| 103 | + JsonNode first = paths.get(0); | |
| 104 | + long distanceMeters = first.path("distance").asLong(-1); | |
| 105 | + long durationSeconds = first.path("cost").path("duration").asLong(-1); | |
| 106 | + if (distanceMeters < 0 || durationSeconds <= 0) { | |
| 107 | + log.warn("Amap electrobike 字段异常 distance={} duration={} body={}", | |
| 108 | + distanceMeters, durationSeconds, trim(response.body())); | |
| 109 | + return Optional.empty(); | |
| 110 | + } | |
| 111 | + return Optional.of(new AmapRoute((int) distanceMeters, (int) durationSeconds)); | |
| 112 | + } catch (Exception e) { | |
| 113 | + log.warn("Amap electrobike 调用失败 err={}", e.getMessage()); | |
| 114 | + return Optional.empty(); | |
| 115 | + } | |
| 116 | + } | |
| 117 | + | |
| 118 | + /** | |
| 119 | + * 返回预计骑行分钟数(向上取整)。委托 {@link #getElectrobikeRoute}。 | |
| 120 | + */ | |
| 121 | + public Optional<Integer> getElectrobikeDurationMinutes(String startLng, String startLat, | |
| 122 | + String endLng, String endLat) { | |
| 123 | + return getElectrobikeRoute(startLng, startLat, endLng, endLat) | |
| 124 | + .map(r -> (int) Math.ceil(r.durationSeconds() / 60.0)); | |
| 125 | + } | |
| 126 | + | |
| 127 | + private String trim(String body) { | |
| 128 | + if (body == null) return ""; | |
| 129 | + return body.length() > 300 ? body.substring(0, 300) : body; | |
| 130 | + } | |
| 131 | +} | ... | ... |
src/main/java/com/diligrp/rider/service/impl/DeliveryFeeServiceImpl.java
| ... | ... | @@ -6,6 +6,7 @@ import com.diligrp.rider.dto.DeliveryPricingRuleDTO; |
| 6 | 6 | import com.diligrp.rider.dto.DeliveryFeeCalcDTO; |
| 7 | 7 | import com.diligrp.rider.service.CityService; |
| 8 | 8 | import com.diligrp.rider.service.DeliveryFeeService; |
| 9 | +import com.diligrp.rider.service.amap.AmapRouteClient; | |
| 9 | 10 | import com.diligrp.rider.util.GeoUtil; |
| 10 | 11 | import com.diligrp.rider.vo.DeliveryFeeResultVO; |
| 11 | 12 | import lombok.RequiredArgsConstructor; |
| ... | ... | @@ -20,6 +21,7 @@ import java.time.ZonedDateTime; |
| 20 | 21 | import java.util.ArrayList; |
| 21 | 22 | import java.util.Comparator; |
| 22 | 23 | import java.util.List; |
| 24 | +import java.util.Optional; | |
| 23 | 25 | |
| 24 | 26 | /** |
| 25 | 27 | * 配送费计算引擎 |
| ... | ... | @@ -31,6 +33,7 @@ import java.util.List; |
| 31 | 33 | public class DeliveryFeeServiceImpl implements DeliveryFeeService { |
| 32 | 34 | |
| 33 | 35 | private final CityService cityService; |
| 36 | + private final AmapRouteClient amapRouteClient; | |
| 34 | 37 | |
| 35 | 38 | @Override |
| 36 | 39 | public DeliveryFeeResultVO calcFee(DeliveryFeeCalcDTO dto) { |
| ... | ... | @@ -182,7 +185,14 @@ public class DeliveryFeeServiceImpl implements DeliveryFeeService { |
| 182 | 185 | |
| 183 | 186 | BigDecimal total = moneyBasic.add(moneyDistance).add(moneyWeight).add(moneyPiece).add(moneyTime); |
| 184 | 187 | result.setTotalFee(applyMinFee(total, typeConfig, result)); |
| 185 | - result.setEstimatedMinutes(calcEstimatedMinutes(pricingConfig, distanceKm)); | |
| 188 | + | |
| 189 | + // ETA:优先取高德电动车路径耗时,失败降级到 distance_basic_time + 公式 | |
| 190 | + Optional<Integer> amapMinutes = amapRouteClient.getElectrobikeDurationMinutes( | |
| 191 | + dto.getStartLng(), dto.getStartLat(), dto.getEndLng(), dto.getEndLat()); | |
| 192 | + int estimatedMinutes = amapMinutes.isPresent() | |
| 193 | + ? amapMinutes.get() | |
| 194 | + : calcEstimatedMinutes(pricingConfig, distanceKm); | |
| 195 | + result.setEstimatedMinutes(estimatedMinutes); | |
| 186 | 196 | |
| 187 | 197 | return result; |
| 188 | 198 | } | ... | ... |
src/main/java/com/diligrp/rider/vo/RouteCalcVO.java
0 → 100644
| 1 | +package com.diligrp.rider.vo; | |
| 2 | + | |
| 3 | +import lombok.AllArgsConstructor; | |
| 4 | +import lombok.Data; | |
| 5 | +import lombok.NoArgsConstructor; | |
| 6 | + | |
| 7 | +/** | |
| 8 | + * 开放平台路线计算结果 VO。 | |
| 9 | + */ | |
| 10 | +@Data | |
| 11 | +@NoArgsConstructor | |
| 12 | +@AllArgsConstructor | |
| 13 | +public class RouteCalcVO { | |
| 14 | + | |
| 15 | + /** 距离(米)。高德命中时为路径距离,否则为直线距离向上取整。 */ | |
| 16 | + private Integer distance; | |
| 17 | + | |
| 18 | + /** 导航耗时(秒)。高德未命中时为 null。 */ | |
| 19 | + private Integer duration; | |
| 20 | +} | ... | ... |
src/main/resources/application.yml
| ... | ... | @@ -32,12 +32,19 @@ jwt: |
| 32 | 32 | expire: 604800 # 7天,单位秒 |
| 33 | 33 | |
| 34 | 34 | jpush: |
| 35 | - enabled: false # 未配置 AppKey 前置 false,避免启动时报错 | |
| 35 | + enabled: true # 未配置 AppKey 前置 false,避免启动时报错 | |
| 36 | 36 | app-key: fd6e826ae6e67eaf7a7062c4 |
| 37 | 37 | master-secret: 6eaa9a8d11493b56812b6ee9 |
| 38 | 38 | apns-production: false # iOS APNs:true=生产 false=开发 |
| 39 | 39 | time-to-live: 86400 # 离线消息保留秒数(默认 1 天) |
| 40 | 40 | |
| 41 | +amap: | |
| 42 | + enabled: true # false 时跳过高德调用,ETA 走直线距离 + 公式 | |
| 43 | + key: 5aa7f0260a51dcb2f1dda3130f9a0dbd # 高德 Web 服务 API Key,可由环境变量注入 | |
| 44 | + base-url: https://restapi.amap.com | |
| 45 | + connect-timeout-millis: 3000 | |
| 46 | + read-timeout-millis: 3000 | |
| 47 | + | |
| 41 | 48 | logging: |
| 42 | 49 | level: |
| 43 | 50 | com.diligrp.rider: debug | ... | ... |
src/test/java/com/diligrp/rider/controller/OpenApiControllerTest.java
0 → 100644
| 1 | +package com.diligrp.rider.controller; | |
| 2 | + | |
| 3 | +import com.diligrp.rider.common.exception.BizException; | |
| 4 | +import com.diligrp.rider.common.result.Result; | |
| 5 | +import com.diligrp.rider.dto.OpenRouteCalcDTO; | |
| 6 | +import com.diligrp.rider.service.DeliveryFeeService; | |
| 7 | +import com.diligrp.rider.service.OpenAppService; | |
| 8 | +import com.diligrp.rider.service.amap.AmapRouteClient; | |
| 9 | +import com.diligrp.rider.util.GeoUtil; | |
| 10 | +import com.diligrp.rider.vo.RouteCalcVO; | |
| 11 | +import org.junit.jupiter.api.Test; | |
| 12 | + | |
| 13 | +import java.util.Optional; | |
| 14 | + | |
| 15 | +import static org.junit.jupiter.api.Assertions.assertEquals; | |
| 16 | +import static org.junit.jupiter.api.Assertions.assertNotNull; | |
| 17 | +import static org.junit.jupiter.api.Assertions.assertNull; | |
| 18 | +import static org.junit.jupiter.api.Assertions.assertThrows; | |
| 19 | +import static org.mockito.ArgumentMatchers.any; | |
| 20 | +import static org.mockito.Mockito.mock; | |
| 21 | +import static org.mockito.Mockito.when; | |
| 22 | + | |
| 23 | +class OpenApiControllerTest { | |
| 24 | + | |
| 25 | + private final DeliveryFeeService deliveryFeeService = mock(DeliveryFeeService.class); | |
| 26 | + private final OpenAppService openAppService = mock(OpenAppService.class); | |
| 27 | + private final AmapRouteClient amapRouteClient = mock(AmapRouteClient.class); | |
| 28 | + private final OpenApiController controller = | |
| 29 | + new OpenApiController(deliveryFeeService, openAppService, amapRouteClient); | |
| 30 | + | |
| 31 | + @Test | |
| 32 | + void routeCalcShouldReturnAmapValuesWhenHit() { | |
| 33 | + OpenRouteCalcDTO dto = dto("121.4737", "31.2304", "121.5067", "31.2454"); | |
| 34 | + when(amapRouteClient.getElectrobikeRoute( | |
| 35 | + dto.getStartLng(), dto.getStartLat(), dto.getEndLng(), dto.getEndLat())) | |
| 36 | + .thenReturn(Optional.of(new AmapRouteClient.AmapRoute(4830, 1080))); | |
| 37 | + | |
| 38 | + Result<RouteCalcVO> result = controller.routeCalc(dto); | |
| 39 | + | |
| 40 | + assertEquals(0, result.getCode()); | |
| 41 | + assertEquals(4830, result.getData().getDistance()); | |
| 42 | + assertEquals(1080, result.getData().getDuration()); | |
| 43 | + } | |
| 44 | + | |
| 45 | + @Test | |
| 46 | + void routeCalcShouldFallbackToGeoUtilWhenAmapMisses() { | |
| 47 | + OpenRouteCalcDTO dto = dto("121.4737", "31.2304", "121.4737", "31.2574"); | |
| 48 | + when(amapRouteClient.getElectrobikeRoute(any(), any(), any(), any())) | |
| 49 | + .thenReturn(Optional.empty()); | |
| 50 | + | |
| 51 | + Result<RouteCalcVO> result = controller.routeCalc(dto); | |
| 52 | + | |
| 53 | + int expected = (int) Math.round( | |
| 54 | + GeoUtil.calcDistanceKm(31.2304, 121.4737, 31.2574, 121.4737) * 1000); | |
| 55 | + assertEquals(0, result.getCode()); | |
| 56 | + assertNotNull(result.getData().getDistance()); | |
| 57 | + assertEquals(expected, result.getData().getDistance()); | |
| 58 | + assertNull(result.getData().getDuration()); | |
| 59 | + } | |
| 60 | + | |
| 61 | + @Test | |
| 62 | + void routeCalcShouldThrowOnInvalidCoordinatesWhenAmapMisses() { | |
| 63 | + OpenRouteCalcDTO dto = dto("not-a-number", "31.2304", "121.5067", "31.2454"); | |
| 64 | + when(amapRouteClient.getElectrobikeRoute(any(), any(), any(), any())) | |
| 65 | + .thenReturn(Optional.empty()); | |
| 66 | + | |
| 67 | + assertThrows(BizException.class, () -> controller.routeCalc(dto)); | |
| 68 | + } | |
| 69 | + | |
| 70 | + private OpenRouteCalcDTO dto(String startLng, String startLat, String endLng, String endLat) { | |
| 71 | + OpenRouteCalcDTO dto = new OpenRouteCalcDTO(); | |
| 72 | + dto.setStartLng(startLng); | |
| 73 | + dto.setStartLat(startLat); | |
| 74 | + dto.setEndLng(endLng); | |
| 75 | + dto.setEndLat(endLat); | |
| 76 | + return dto; | |
| 77 | + } | |
| 78 | +} | ... | ... |
src/test/java/com/diligrp/rider/service/impl/DeliveryFeeServiceImplTest.java
| ... | ... | @@ -4,6 +4,7 @@ import com.diligrp.rider.dto.DeliveryPricingConfigDTO; |
| 4 | 4 | import com.diligrp.rider.dto.DeliveryPricingRuleDTO; |
| 5 | 5 | import com.diligrp.rider.dto.DeliveryFeeCalcDTO; |
| 6 | 6 | import com.diligrp.rider.service.CityService; |
| 7 | +import com.diligrp.rider.service.amap.AmapRouteClient; | |
| 7 | 8 | import com.diligrp.rider.util.GeoUtil; |
| 8 | 9 | import com.diligrp.rider.vo.DeliveryFeeResultVO; |
| 9 | 10 | import org.junit.jupiter.api.Test; |
| ... | ... | @@ -14,13 +15,19 @@ import java.time.LocalDateTime; |
| 14 | 15 | import java.time.ZoneId; |
| 15 | 16 | import java.util.Arrays; |
| 16 | 17 | import java.util.List; |
| 18 | +import java.util.Optional; | |
| 17 | 19 | |
| 18 | 20 | import static org.junit.jupiter.api.Assertions.assertEquals; |
| 21 | +import static org.mockito.ArgumentMatchers.any; | |
| 19 | 22 | import static org.mockito.Mockito.mock; |
| 23 | +import static org.mockito.Mockito.never; | |
| 24 | +import static org.mockito.Mockito.verify; | |
| 25 | +import static org.mockito.Mockito.when; | |
| 20 | 26 | |
| 21 | 27 | class DeliveryFeeServiceImplTest { |
| 22 | 28 | |
| 23 | - private final DeliveryFeeServiceImpl service = new DeliveryFeeServiceImpl(mock(CityService.class)); | |
| 29 | + private final AmapRouteClient amapRouteClient = mock(AmapRouteClient.class); | |
| 30 | + private final DeliveryFeeServiceImpl service = new DeliveryFeeServiceImpl(mock(CityService.class), amapRouteClient); | |
| 24 | 31 | |
| 25 | 32 | @Test |
| 26 | 33 | void shouldApplyMinFee() { |
| ... | ... | @@ -126,6 +133,52 @@ class DeliveryFeeServiceImplTest { |
| 126 | 133 | assertEquals(new BigDecimal("2.50"), result.getTotalFee()); |
| 127 | 134 | } |
| 128 | 135 | |
| 136 | + @Test | |
| 137 | + void shouldUseAmapDurationWhenAvailable() { | |
| 138 | + DeliveryPricingConfigDTO pricingConfig = defaultPricingConfig(); | |
| 139 | + pricingConfig.getType6().setDistanceSwitch(0); | |
| 140 | + pricingConfig.getType6().setWeightSwitch(0); | |
| 141 | + | |
| 142 | + DeliveryFeeCalcDTO calc = baseCalc(); | |
| 143 | + calc.setEndLat("31.2574"); | |
| 144 | + calc.setEndLng("121.4737"); | |
| 145 | + when(amapRouteClient.getElectrobikeDurationMinutes( | |
| 146 | + calc.getStartLng(), calc.getStartLat(), calc.getEndLng(), calc.getEndLat())) | |
| 147 | + .thenReturn(Optional.of(18)); | |
| 148 | + | |
| 149 | + DeliveryFeeResultVO result = service.calcFeeByConfig(pricingConfig, calc); | |
| 150 | + | |
| 151 | + assertEquals(18, result.getEstimatedMinutes()); | |
| 152 | + } | |
| 153 | + | |
| 154 | + @Test | |
| 155 | + void shouldFallbackToFormulaWhenAmapEmpty() { | |
| 156 | + DeliveryPricingConfigDTO pricingConfig = defaultPricingConfig(); | |
| 157 | + pricingConfig.getType6().setDistanceSwitch(0); | |
| 158 | + pricingConfig.getType6().setWeightSwitch(0); | |
| 159 | + | |
| 160 | + when(amapRouteClient.getElectrobikeDurationMinutes(any(), any(), any(), any())) | |
| 161 | + .thenReturn(Optional.empty()); | |
| 162 | + | |
| 163 | + DeliveryFeeResultVO result = service.calcFeeByConfig(pricingConfig, baseCalc()); | |
| 164 | + | |
| 165 | + // distanceKm=0 时低于 distance_basic=3,应取基础时长 distance_basic_time=30 | |
| 166 | + assertEquals(30, result.getEstimatedMinutes()); | |
| 167 | + } | |
| 168 | + | |
| 169 | + @Test | |
| 170 | + void shouldNotCallAmapForFixedFee() { | |
| 171 | + DeliveryPricingConfigDTO pricingConfig = defaultPricingConfig(); | |
| 172 | + pricingConfig.getType6().setFeeMode(1); | |
| 173 | + pricingConfig.getType6().setFixMoney(new BigDecimal("8.00")); | |
| 174 | + | |
| 175 | + DeliveryFeeResultVO result = service.calcFeeByConfig(pricingConfig, baseCalc()); | |
| 176 | + | |
| 177 | + assertEquals(new BigDecimal("8.00"), result.getTotalFee()); | |
| 178 | + assertEquals(30, result.getEstimatedMinutes()); | |
| 179 | + verify(amapRouteClient, never()).getElectrobikeDurationMinutes(any(), any(), any(), any()); | |
| 180 | + } | |
| 181 | + | |
| 129 | 182 | private DeliveryPricingConfigDTO defaultPricingConfig() { |
| 130 | 183 | DeliveryPricingConfigDTO config = new DeliveryPricingConfigDTO(); |
| 131 | 184 | config.setType(Arrays.asList(6)); | ... | ... |