Эх сурвалжийг харах

fix: 明确闪送基础配送费语义

qmj 18 цаг өмнө
parent
commit
8feb398a5f

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

@@ -45,7 +45,7 @@ public class FlashDeliveryOrderView {
     private String distanceSource;
     /** 预计配送时长(秒)。 */
     private Integer estimatedDurationSeconds;
-    /** 基础配送费(TWD),已含距离费。 */
+    /** 基础配送费(TWD),仅表示起步价,不含距离附加费。 */
     private Long baseDeliveryFee;
     /** 距离附加费(TWD)。 */
     private Long distanceFee;

+ 2 - 2
ruoyi-admin/src/main/java/com/ruoyi/app/flashdelivery/dto/FlashDeliveryPriceBreakdown.java

@@ -11,12 +11,12 @@ public class FlashDeliveryPriceBreakdown {
     private BigDecimal billableDistance;
     /** 距离附加费(TWD)。 */
     private Long distanceFee;
-    /** 基础配送费(TWD),等于起步价加距离费。 */
+    /** 基础配送费(TWD),仅表示起步价,不含距离附加费。 */
     private Long baseDeliveryFee;
     /** 加急费(TWD),普通配送为 0。 */
     private Long urgentFee;
     /** 小费(TWD)。 */
     private Long tipAmount;
-    /** 总金额(TWD),等于基础配送费、加费、小费之和。 */
+    /** 总金额(TWD),等于基础配送费、距离附加费、加急费和小费之和。 */
     private Long amount;
 }

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

@@ -44,9 +44,9 @@ public class FlashDeliveryQuoteView {
     private Long freight;
     /** 计费距离(公里):超出起步距离部分的计费公里数,不足 0.5 公里为 0、0.5 至 1 公里记 1 公里、1 公里以上按实际公里数。 */
     private BigDecimal billableDistance;
-    /** 距离附加费(TWD),已包含在基础配送费中。 */
+    /** 距离附加费(TWD)。 */
     private Long distanceFee;
-    /** 基础配送费(TWD),等于起步价加距离费。 */
+    /** 基础配送费(TWD),仅表示起步价,不含距离附加费。 */
     private Long baseDeliveryFee;
     /** 加急费比例(百分比)。 */
     private BigDecimal urgentRate;

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

@@ -49,7 +49,7 @@ public class FlashDeliveryRiderOrderListView {
     private Integer distanceMeters;
     /** 预计配送时长(秒)。 */
     private Integer estimatedDurationSeconds;
-    /** 基础配送费(TWD),已含距离费。 */
+    /** 基础配送费(TWD),仅表示起步价,不含距离附加费。 */
     private Long baseDeliveryFee;
     /** 距离附加费(TWD)。 */
     private Long distanceFee;

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

@@ -43,7 +43,7 @@ public class FlashDeliveryUserOrderListView {
     private String deliveryDetailAddress;
     /** 预计配送时长(秒)。 */
     private Integer estimatedDurationSeconds;
-    /** 基础配送费(TWD),已含距离费。 */
+    /** 基础配送费(TWD),仅表示起步价,不含距离附加费。 */
     private Long baseDeliveryFee;
     /** 加急费(TWD),普通配送为 0。 */
     private Long urgentFee;

+ 7 - 5
ruoyi-admin/src/main/java/com/ruoyi/app/flashdelivery/service/FlashDeliveryPricingCalculator.java

@@ -21,9 +21,9 @@ public class FlashDeliveryPricingCalculator {
     }
 
     /**
-     * 费用明细计算:基础配送费 = 起步价 + 距离费;距离费按超起步距离部分、以步长折算的
+     * 费用明细计算:基础配送费仅为起步价;距离费按超起步距离部分、以步长折算的
      * 每公里单价线性计价,不足 0.5 公里不计、0.5 至 1 公里按 1 公里计;加急费仅 URGENT 收取,
-     * 取最低加急费与基础配送费乘加急比例的较大者;总金额 = 基础配送费 + 加急费 + 小费,均为整数 TWD。
+     * 按起步价和距离费之和计算;总金额 = 基础配送费 + 距离费 + 加急费 + 小费,均为整数 TWD。
      */
     public FlashDeliveryPriceBreakdown calculateBreakdown(FlashDeliveryPricing pricing, int distanceMeters,
                                                            String deliveryType, Long tipAmount) {
@@ -60,9 +60,11 @@ public class FlashDeliveryPricingCalculator {
         FlashDeliveryPriceBreakdown result = new FlashDeliveryPriceBreakdown();
         result.setBillableDistance(chargeableDistance.setScale(2, RoundingMode.HALF_UP));
         result.setDistanceFee(distanceFee);
-        long baseDeliveryFee = Math.addExact(pricing.getStartingFare(), distanceFee);
+        // 对外基础配送费固定表示起步价;普通配送费仅作内部计算,不单独落库或返回。
+        long baseDeliveryFee = pricing.getStartingFare();
+        long ordinaryDeliveryFee = Math.addExact(baseDeliveryFee, distanceFee);
         long urgentFee = "URGENT".equals(deliveryType)
-                ? Math.max(pricing.getMinimumUrgentFee(), BigDecimal.valueOf(baseDeliveryFee)
+                ? Math.max(pricing.getMinimumUrgentFee(), BigDecimal.valueOf(ordinaryDeliveryFee)
                 .multiply(pricing.getUrgentRate())
                 .divide(BigDecimal.valueOf(100), 0, RoundingMode.HALF_UP)
                 .longValueExact())
@@ -70,7 +72,7 @@ public class FlashDeliveryPricingCalculator {
         result.setBaseDeliveryFee(baseDeliveryFee);
         result.setUrgentFee(urgentFee);
         result.setTipAmount(tipAmount);
-        result.setAmount(Math.addExact(Math.addExact(baseDeliveryFee, urgentFee), tipAmount));
+        result.setAmount(Math.addExact(Math.addExact(ordinaryDeliveryFee, urgentFee), tipAmount));
         return result;
     }
 }

+ 2 - 2
ruoyi-admin/src/test/java/com/ruoyi/app/flashdelivery/service/FlashDeliveryApplicationServiceTest.java

@@ -806,7 +806,7 @@ class FlashDeliveryApplicationServiceTest {
         var quote = fixture.service.quote(request);
 
         assertEquals(45L, quote.getDistanceFee());
-        assertEquals(135L, quote.getBaseDeliveryFee());
+        assertEquals(90L, quote.getBaseDeliveryFee());
         assertEquals(27L, quote.getUrgentFee());
         assertEquals(5L, quote.getTipAmount());
         assertEquals(167L, quote.getAmount());
@@ -1165,7 +1165,7 @@ class FlashDeliveryApplicationServiceTest {
         assertEquals(4500, quote.getDistanceMeters());
         assertEquals(900, quote.getEstimatedDurationSeconds());
         assertEquals(23L, quote.getDistanceFee());
-        assertEquals(113L, quote.getBaseDeliveryFee());
+        assertEquals(90L, quote.getBaseDeliveryFee());
         assertEquals(10L, quote.getTipAmount());
         assertEquals(123L, quote.getAmount());
         assertEquals(2, quote.getOrderVersion());

+ 2 - 2
ruoyi-admin/src/test/java/com/ruoyi/app/flashdelivery/service/FlashDeliveryPricingCalculatorTest.java

@@ -53,11 +53,11 @@ class FlashDeliveryPricingCalculatorTest {
     }
 
     @Test
-    void calculatesUrgentFeeFromBaseDeliveryFeeAndAddsTipSeparately() {
+    void exposesStartingFareAsBaseDeliveryFeeAndAddsDistanceFeeSeparately() {
         var breakdown = calculator.calculateBreakdown(pricing(), 6000, "URGENT", 5L);
 
         assertEquals(45L, breakdown.getDistanceFee());
-        assertEquals(135L, breakdown.getBaseDeliveryFee());
+        assertEquals(90L, breakdown.getBaseDeliveryFee());
         assertEquals(27L, breakdown.getUrgentFee());
         assertEquals(5L, breakdown.getTipAmount());
         assertEquals(167L, breakdown.getAmount());

+ 1 - 1
ruoyi-system/src/main/java/com/ruoyi/system/domain/flash/FlashDeliveryOrder.java

@@ -100,7 +100,7 @@ public class FlashDeliveryOrder {
     private Long freight;
     /** 距离附加费(TWD),按超起步距离部分以每公里单价计算。 */
     private Long distanceFee;
-    /** 基础配送费(TWD),等于起步价加距离费。 */
+    /** 基础配送费(TWD),仅表示起步价,不含距离附加费。 */
     private Long baseDeliveryFee;
     /** 加急费比例(百分比)。 */
     private BigDecimal urgentRate;

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

@@ -43,7 +43,7 @@
 
 用户列表摘要字段固定为:`id,orderNo,serviceType,deliveryType,vehicleType,status,packageType,quantity,weightRange,deliveryMode,scheduledPickupStartAt,scheduledPickupEndAt,pickupAddress,pickupDetailAddress,deliveryAddress,deliveryDetailAddress,estimatedDurationSeconds,baseDeliveryFee,urgentFee,tipAmount,amount,currency,deliveredAt,createTime`。列表不返回联系人、电话、精确坐标、照片、日志或内部计价审计字段。`receiver` 不是按当前手机号动态查询:订单只在创建时唯一匹配已注册普通用户并固化 `receiverUserId`,未匹配订单以后不追溯认领。
 
-报价响应 data:`serviceType,deliveryType,vehicleType,deliveryMode,scheduledPickupStartAt?,scheduledPickupEndAt?,pricingId,startTime,endTime,distanceMeters,distanceSource,estimatedDurationSeconds?,startingDistance,startingFare,distance,freight,billableDistance,distanceFee,baseDeliveryFee,urgentRate,minimumUrgentFee,urgentFee,tipAmount,amount,currency,pricingVersion`。`billableDistance` 为按外卖规则处理 0.5 公里边界后的计费公里数;全部费用为整数 TWD,`urgentRate` 为百分比。`baseDeliveryFee=startingFare+distanceFee`;普通配送 `urgentFee=0`;加急配送 `urgentFee=max(minimumUrgentFee,roundHalfUp(baseDeliveryFee*urgentRate/100))`;`amount=baseDeliveryFee+urgentFee+tipAmount`。
+报价响应 data:`serviceType,deliveryType,vehicleType,deliveryMode,scheduledPickupStartAt?,scheduledPickupEndAt?,pricingId,startTime,endTime,distanceMeters,distanceSource,estimatedDurationSeconds?,startingDistance,startingFare,distance,freight,billableDistance,distanceFee,baseDeliveryFee,urgentRate,minimumUrgentFee,urgentFee,tipAmount,amount,currency,pricingVersion`。`billableDistance` 为按外卖规则处理 0.5 公里边界后的计费公里数;全部费用为整数 TWD,`urgentRate` 为百分比。`baseDeliveryFee=startingFare`,仅表示起步价;普通配送费在服务端按 `baseDeliveryFee+distanceFee` 计算但不单独返回;普通配送 `urgentFee=0`;加急配送 `urgentFee=max(minimumUrgentFee,roundHalfUp((baseDeliveryFee+distanceFee)*urgentRate/100))`;`amount=baseDeliveryFee+distanceFee+urgentFee+tipAmount`。已有订单的金额快照不回写,新口径仅用于此调整发布后的新报价和新订单。
 
 创建时服务端按 `vehicleType` 重新获取对应车型路线、匹配运价并复算费用。`pricingId`、`pricingVersion`、`quotedBaseDeliveryFee`、`quotedDistanceFee`、`quotedUrgentFee`、`quotedAmount` 任一不一致时不创建订单,返回国际化“报价已变化,请重新确认”及完整最新报价。地址快照字段为联系人、电话、市/区、交付方式、地址、详细地址和经纬度。用户创建和详情响应的 `data` 直接为订单详情字段,照片分别为 `senderImageUrls,pickupImageUrls,deliveryImageUrls`,不返回原始状态日志;`deliveryPinCode` 仅在启用时向订单参与用户返回,用户仅在 `ACCEPTED`、`PICKED_UP` 阶段获得骑手位置。
 
@@ -134,7 +134,7 @@
 }
 ```
 
-金额仅为示例,客户端必须使用实际报价;`distanceFee` 已包含于 `baseDeliveryFee`,总金额为 `baseDeliveryFee + urgentFee + tipAmount`。
+金额仅为示例,客户端必须使用实际报价;`baseDeliveryFee` 仅为起步价,`distanceFee` 为独立距离附加费,总金额为 `baseDeliveryFee + distanceFee + urgentFee + tipAmount`。
 
 保存前服务端重新计算路线和价格。距离或任一确认金额不同(包括漏传)时,返回非成功 AjaxResult,`msg` 为国际化 `flash.delivery.quote.changed`,`data` 为最新完整报价,订单不变。客户端展示最新报价,经用户再次确认后重新提交,不能自动按新报价扣改金额。
 

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

@@ -251,7 +251,7 @@ COMPLETED
 
 平台每个时间段配置 `startTime`、`endTime`、`startingDistance`、`startingFare`、`distance`、`freight`、`urgentRate` 和 `minimumUrgentFee`。所有时间段全局互斥,不再按 `serviceType` 分组。立即订单按报价时刻匹配,预约订单按 `scheduledPickupStartAt` 匹配;日期只用于确定预约有效性,时间段按业务当地时间的时分命中。
 
-普通配送费先沿用现有起送与超距规则计算:`baseDeliveryFee = startingFare + distanceFee`。普通配送 `urgentFee=0`;加急配送使用 `urgentFee = max(minimumUrgentFee, roundHalfUp(baseDeliveryFee × urgentRate / 100))`。骑手小费 `tipAmount` 是用户输入的独立非负整数 TWD,不参与加急费计算;最终 `amount = baseDeliveryFee + urgentFee + tipAmount`。
+基础配送费固定为起步价:`baseDeliveryFee = startingFare`;普通配送费只在服务端按 `baseDeliveryFee + distanceFee` 计算,不单独保存或返回。普通配送 `urgentFee=0`;加急配送使用 `urgentFee = max(minimumUrgentFee, roundHalfUp((baseDeliveryFee + distanceFee) × urgentRate / 100))`。骑手小费 `tipAmount` 是用户输入的独立非负整数 TWD,不参与加急费计算;最终 `amount = baseDeliveryFee + distanceFee + urgentFee + tipAmount`。
 
 ### 12.3 报价回传与创建校验
 

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

@@ -239,6 +239,10 @@ foodie-admin-vue/
 6. `home` 对三种服务分别读取当前时段,只返回有配置的服务;`quote/create` 每次重新读取当前时段并计算。订单固化 `pricingId,pricingVersion,pricingStartTime,pricingEndTime,startingDistance,startingFare,distance,freight,distanceFee`。
 7. 报价与公开服务 DTO 移除旧计价字段,保留 `estimatedDurationSeconds`;金额 DTO 和订单实体改为整数 `Long`。
 
+### 2026-09-17 基础配送费口径调整
+
+`baseDeliveryFee` 统一表示起步价,`distanceFee` 保持独立距离附加费;不增加配送小计字段。服务端计算加急费时仍使用起步价与距离附加费之和,订单总额为基础配送费、距离附加费、加急费与小费之和。既有订单快照不回写。
+
 ### 平台前端交互
 
 - 价格页表格按服务类型、开始时间排序,列出起止时间、起送距离/价格、计价距离/金额、版本和更新时间。

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

@@ -240,9 +240,9 @@
 - **FR-067**:闪送继续保持非支付业务,创建成功直接进入 `WAITING_ACCEPTANCE`,不增加待支付状态、支付倒计时、支付接口或退款流程。
 - **FR-068**:所有闪送金额继续使用整数 TWD;蓝湖稿中的小数金额仅为视觉占位,不改变接口和数据库金额类型。
 - **FR-069**:用户端不得展示固定或伪造的附近骑手人数及预计接单分钟数,统一使用“发布后等待附近骑手接单”;本期不增加实时骑手数量或接单预测接口。
-- **FR-070**:普通配送费等于起送价格与距离费之和;普通配送的加急费为 0,加急配送的加急费等于“普通配送费 × 当前时段加急比例”四舍五入到整数 TWD 后与最低加急费取较大值。
-- **FR-071**:骑手小费由用户提交,为独立的非负整数 TWD;订单总金额等于普通配送费、加急费和骑手小费之和,小费不得参与加急费计算。
-- **FR-072**:报价和订单响应必须分别返回 `baseDeliveryFee`、`distanceFee`、`urgentFee`、`tipAmount` 和 `amount`;加急报价还必须返回命中的 `urgentRate` 和 `minimumUrgentFee`,费用名称不得混用。
+- **FR-070**:`baseDeliveryFee` 仅表示起送价格,`distanceFee` 表示距离附加费;普通配送费仅在服务端按两者之和计算,不增加小计字段。普通配送的加急费为 0,加急配送的加急费等于“普通配送费 × 当前时段加急比例”四舍五入到整数 TWD 后与最低加急费取较大值。
+- **FR-071**:骑手小费由用户提交,为独立的非负整数 TWD;订单总金额等于基础配送费、距离附加费、加急费和骑手小费之和,小费不得参与加急费计算。
+- **FR-072**:报价和订单响应必须分别返回 `baseDeliveryFee`、`distanceFee`、`urgentFee`、`tipAmount` 和 `amount`;加急报价还必须返回命中的 `urgentRate` 和 `minimumUrgentFee`,费用名称不得混用。历史订单金额快照不回写。
 
 ### 关键实体