Procházet zdrojové kódy

fix: 补全闪送降级预计时长

qmj před 6 hodinami
rodič
revize
2f2b86b930

+ 1 - 1
ruoyi-admin/src/main/java/com/ruoyi/app/flashdelivery/dto/FlashDeliveryQuoteView.java

@@ -32,7 +32,7 @@ public class FlashDeliveryQuoteView {
     private Integer distanceMeters;
     /** 距离来源:ROUTE=地图路线、STRAIGHT_LINE=降级直线。 */
     private String distanceSource;
-    /** 预计配送时长(秒),直线降级时为空。 */
+    /** 预计配送时长(秒);直线降级时按车型保守速度估算。 */
     private Integer estimatedDurationSeconds;
     /** 起步距离(公里)。 */
     private BigDecimal startingDistance;

+ 16 - 1
ruoyi-admin/src/main/java/com/ruoyi/app/flashdelivery/route/FlashDeliveryRouteService.java

@@ -9,6 +9,9 @@ import org.springframework.stereotype.Service;
 @Service
 public class FlashDeliveryRouteService {
     private static final double EARTH_RADIUS_METERS = 6_371_000D;
+    private static final int MOTORCYCLE_FALLBACK_SPEED_KPH = 25;
+    private static final int CAR_FALLBACK_SPEED_KPH = 30;
+    private static final int MINIMUM_FALLBACK_DURATION_SECONDS = 60;
     private static final Logger log = LoggerFactory.getLogger(FlashDeliveryRouteService.class);
     private final RouteDistanceProvider provider;
 
@@ -43,7 +46,19 @@ public class FlashDeliveryRouteService {
                 + Math.cos(lat1) * Math.cos(lat2)
                 * Math.sin(deltaLon / 2) * Math.sin(deltaLon / 2);
         int meters = (int) Math.round(EARTH_RADIUS_METERS * 2D * Math.atan2(Math.sqrt(a), Math.sqrt(1D - a)));
-        return new RouteDistance(Math.max(1, meters), null, "STRAIGHT_LINE");
+        int straightLineMeters = Math.max(1, meters);
+        return new RouteDistance(straightLineMeters, calculateFallbackDurationSeconds(straightLineMeters, selectedVehicle),
+                "STRAIGHT_LINE");
+    }
+
+    /**
+     * 地图路线不可用时,按车型保守平均速度估算履约展示时长;该值不参与闪送计价。
+     */
+    private int calculateFallbackDurationSeconds(int distanceMeters, int vehicleType) {
+        int speedKph = vehicleType == FlashDeliveryVehicleType.CAR
+                ? CAR_FALLBACK_SPEED_KPH : MOTORCYCLE_FALLBACK_SPEED_KPH;
+        double seconds = distanceMeters * 3_600D / (speedKph * 1_000D);
+        return Math.max(MINIMUM_FALLBACK_DURATION_SECONDS, (int) Math.ceil(seconds));
     }
 
     private void validate(GeoPoint point) {

+ 26 - 0
ruoyi-admin/src/test/java/com/ruoyi/app/flashdelivery/route/FlashDeliveryRouteServiceTest.java

@@ -51,6 +51,32 @@ class FlashDeliveryRouteServiceTest {
         assertEquals("STRAIGHT_LINE", result.source());
     }
 
+    @Test
+    void estimatesStraightLineFallbackDurationBySelectedVehicle() {
+        RouteDistanceProvider provider = (a, b, c, d, vehicleType) -> { throw new IllegalStateException("down"); };
+        FlashDeliveryRouteService service = new FlashDeliveryRouteService(provider);
+
+        RouteDistance motorcycle = service.calculate(point("0", "0"), point("0", "0.01"),
+                FlashDeliveryVehicleType.MOTORCYCLE);
+        RouteDistance car = service.calculate(point("0", "0"), point("0", "0.01"),
+                FlashDeliveryVehicleType.CAR);
+
+        assertEquals("STRAIGHT_LINE", motorcycle.source());
+        assertEquals(161, motorcycle.durationSeconds());
+        assertEquals(134, car.durationSeconds());
+    }
+
+    @Test
+    void keepsStraightLineFallbackDurationAtLeastOneMinute() {
+        RouteDistanceProvider provider = (a, b, c, d, vehicleType) -> { throw new IllegalStateException("down"); };
+        FlashDeliveryRouteService service = new FlashDeliveryRouteService(provider);
+
+        RouteDistance result = service.calculate(point("0", "0"), point("0", "0.000001"),
+                FlashDeliveryVehicleType.MOTORCYCLE);
+
+        assertEquals(60, result.durationSeconds());
+    }
+
     private GeoPoint point(String latitude, String longitude) {
         return new GeoPoint(new BigDecimal(latitude), new BigDecimal(longitude));
     }

+ 1 - 1
specs/024-flash-delivery/contracts/api.md

@@ -11,7 +11,7 @@
 - `packageType`:`DOCUMENT`、`GIFT`、`CLOTHING`、`BEAUTY`、`DAILY_NECESSITIES`、`FOOD_INGREDIENTS`、`ELECTRONICS`、`SMALL_APPLIANCE`、`OTHER`。
 - 物品信息提交 `packageType`、正整数 `quantity`、可选 `weightRange` 及最长 255 字符的可选 `specification`;传入时 `weightRange` 只接受 `UP_TO_5_KG`、`OVER_5_TO_10_KG`、`OVER_10_TO_15_KG`、`OVER_15_TO_20_KG`。
 - `vehicleType`:`1`=机车、`2`=轿车;报价和创建均必填并必须一致。历史订单、骑手资料和运价配置的空值按机车处理。
-- 路线距离和 `estimatedDurationSeconds` 按 `vehicleType` 获取:机车使用 Google Routes 的 `TWO_WHEELER`,轿车使用 `DRIVE`。两轮路线是 Google Beta 能力;本期不修改用户 App 的提示文案。
+- 路线距离和 `estimatedDurationSeconds` 按 `vehicleType` 获取:机车使用 Google Routes 的 `TWO_WHEELER`,轿车使用 `DRIVE`。地图路线不可用时,`distanceSource=STRAIGHT_LINE`,`estimatedDurationSeconds` 仍必返:机车以 25 km/h、轿车以 30 km/h 的保守速度按直线距离估算,向上取整且最少 60 秒;该时长不参与金额计算。两轮路线是 Google Beta 能力;本期不修改用户 App 的提示文案。
 - `deliveryMode`:`NOW`、`SCHEDULED`。预约起止时间使用 ISO 日期时间,时段固定 30 分钟且开始时间不晚于三天后。
 - `status`:`WAITING_ACCEPTANCE`、`ACCEPTED`、`PICKED_UP`、`DELIVERED`、`COMPLETED`、`CANCELLED`。
 

+ 4 - 0
specs/024-flash-delivery/plan.md

@@ -243,6 +243,10 @@ foodie-admin-vue/
 
 `baseDeliveryFee` 统一表示起步价,`distanceFee` 保持独立距离附加费;不增加配送小计字段。服务端计算加急费时仍使用起步价与距离附加费之和,订单总额为基础配送费、距离附加费、加急费与小费之和。既有订单快照不回写。
 
+### 2026-09-17 路线降级预计时长
+
+Google Routes 无路线、异常或不可用时,`FlashDeliveryRouteService` 继续以 Haversine 直线距离并写入 `distanceSource=STRAIGHT_LINE`。同时按订单车型生成 `estimatedDurationSeconds`:机车按 25 km/h,轿车按 30 km/h,结果向上取整且最少 60 秒。该值仅用于履约展示,不参与报价或订单金额计算;不新增来源字段,既有 `distanceSource` 即可标识降级估算。
+
 ### 平台前端交互
 
 - 价格页表格按服务类型、开始时间排序,列出起止时间、起送距离/价格、计价距离/金额、版本和更新时间。

+ 1 - 1
specs/024-flash-delivery/research.md

@@ -8,7 +8,7 @@
 
 ## 降级与精度
 
-外部路线不可用时使用 Haversine 公式计算球面直线距离,四舍五入为整数米并标记 `STRAIGHT_LINE`;路线成功标记 `ROUTE`。闪送计价与现有外卖订单保持同一业务规则:按当前时间匹配运价时段,起送距离内收起送价格,超出不足 0.5 公里不加价、0.5 至不足 1 公里按 1 公里计、至少 1 公里按实际超出距离计。距离与单价除法使用 `BigDecimal`,里程费用使用 `HALF_UP` 四舍五入为整数新台币;路线预计时长只用于履约展示,不参与金额计算。
+外部路线不可用时使用 Haversine 公式计算球面直线距离,四舍五入为整数米并标记 `STRAIGHT_LINE`;路线成功标记 `ROUTE`。直线降级仍返回预计时长:机车按 25 km/h、轿车按 30 km/h 计算,结果向上取整且最少 60 秒;这是保守履约展示估算,不参与计价。闪送计价与现有外卖订单保持同一业务规则:按当前时间匹配运价时段,起送距离内收起送价格,超出不足 0.5 公里不加价、0.5 至不足 1 公里按 1 公里计、至少 1 公里按实际超出距离计。距离与单价除法使用 `BigDecimal`,里程费用使用 `HALF_UP` 四舍五入为整数新台币;路线预计时长只用于履约展示,不参与金额计算。
 
 ## 并发与安全
 

+ 1 - 1
specs/024-flash-delivery/spec.md

@@ -360,7 +360,7 @@
 
 - 用户报价与创建闪送订单均传 `vehicleType`:`1`=机车、`2`=轿车;服务端拒绝缺失或其他数值。
 - 闪送运价按“车型 + 时段”分别配置;报价、创建及待接单改址均以订单车型命中运价,订单保存车型与既有价格快照,后续调价不改变历史订单。
-- 报价、创建及待接单改址调用 Google Routes 时同样传订单车型:机车使用 `TWO_WHEELER`,轿车使用 `DRIVE`;返回的路线距离和预计时长随订单保存。地图失败仍仅降级为直线距离,不伪造预计时长
+- 报价、创建及待接单改址调用 Google Routes 时同样传订单车型:机车使用 `TWO_WHEELER`,轿车使用 `DRIVE`;返回的路线距离和预计时长随订单保存。地图失败时降级为直线距离并标记 `STRAIGHT_LINE`,预计时长按机车 25 km/h、轿车 30 km/h 保守估算,向上取整且最少 60 秒;该展示估算不参与计价
 - 骑手沿用既有 `info_user.vehicle_type`(`1`=机车、`2`=轿车);待抢列表、待抢详情和接单均要求与订单车型一致。历史骑手、运价和订单均默认机车,保证迁移后既有行为不变。
 - 重量范围与体积/规格说明改为选填;未传时保存 `NULL`,不参与本期运费计算。物品类别、数量、小费及地址校验保持必填规则。
 - 本期不修改用户 App 界面;后续 App 仅需在报价和创建请求都传相同的 `vehicleType`。