Procházet zdrojové kódy

feat: 闪送线下转账送达后展示骑手启用银行卡(方案A)

- 详情视图新增 riderBankCard 三要素(户名/银行名/账号),仅寄件人视角
  且 payType=6 已送达/已完成时返回(FlashDeliveryRiderBankCardView)
- 闸门就绪度:骑手维度线下转账在骑手身份已知时要求启用卡,
  用户下单(骑手未定)保持放行
- 抢单过滤统一走闸门(传 riderId + isReady 过滤),顺带修复原 CSV
  分支绕过平台开关的不一致
- 骑手收款方式接口传 riderId:无卡骑手线下转账 ready=false
- docs:台湾二维码/TWQR 讨论记录(生成放 App 端,后端只出三要素)
- TDD:闸门+1、闪送+5 用例;全量 746/746 通过
qmj před 1 dnem
rodič
revize
ab54a56521

+ 38 - 0
docs/twqr-offline-transfer-qr.md

@@ -0,0 +1,38 @@
+# 闪送线下转账 × 二维码方案讨论记录(2026-09-23)
+
+## 背景
+
+闪送线下转账(payType=6)功能:骑手可维护银行卡(031 已有 `/system/bankCard` 商家/骑手通用接口),
+订单送达后向寄件人展示骑手启用卡三要素(户名/银行名/账号),用户线下转账结清,骑手
+`confirmPayment` 确认收款(payment_status→1)。方案已定 A:到付与线下转账保持两个独立支付方式,
+卡信息仅对 payType=6 的已送达单展示。
+
+讨论主题:能否把银行卡信息包成二维码给用户扫码转账。
+
+## 台湾二维码互通现状
+
+- **TWQR(臺灣 QR Code 共通支付標準)**:财金公司偕同各银行推动的共通机制,基于 EMVCo
+  QRCPS 规范(标签-长度-值 + CRC16)。同一张码可被所有支援的银行 App(台湾 Pay 体系涵盖
+  39 家银行)与电子支付 App(街口、全支付、悠游付等)扫描,跨行通用,支持缴费/转账/消费。
+- **个人转账 QR**:各银行 App 自家"收款码"格式不统一;普通文本(银行代号+账号)编成的
+  非标准码只有部分银行 App 的"扫码转账"能解析,无 100% 保证。
+- TWQR 收款码申请主体目前**多为商家/特店**,个人骑手大概率申请不到。
+
+## 讨论结论
+
+1. **格式层面可行**:二维码本质是包装信息,拿到 TWQR 规格书或足够样例即可按规范生成字符串,
+   任意 QR 库渲染成图——生成不是难点。
+2. **清算层面是真门槛**:码指向的收款账号必须先在财金/银行体系注册 TWQR 收款资格,
+   否则格式正确也收不到钱。个人收款走不通 TWQR。
+3. **产品路径**:
+   - 保底:显示文字卡信息(银行名/账号/户名)——台湾跨行转账 100% 通用
+   - 增强(可选):① App 前端本地编码二维码(通用性不保证);② 骑手上传银行 App 生成的
+     收款码截图(银行自生成的码格式保证,送达后与卡信息一起展示)
+4. **工程结论**:后端只需返回 `bankName/accountNo/accountName` 三要素(已实现于
+   `FlashDeliveryRiderBankCardView`),二维码生成放在 App 端做——格式迭代不用动后端。
+
+## 参考
+
+- [Dacard - TWQR 是什麼?怎麼用?](https://dacard.tw)
+- [PayMe.tw - TWQR 台灣共用支付碼](https://payme.tw)
+- [玉山銀行 - 行動銀行付款碼(台灣Pay/TWQR)使用說明](https://www.esunbank.com.tw)

+ 3 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/flashdelivery/dto/FlashDeliveryOrderDetailView.java

@@ -39,4 +39,7 @@ public class FlashDeliveryOrderDetailView extends FlashDeliveryOrderView {
     private Long currentTargetStopId;
     /** 目标调整记录,骑手声明与实际取货凭证分别展示。 */
     private List<FlashDeliveryTargetAdjustment> targetAdjustments = List.of();
+    /** 骑手启用收款卡三要素;仅寄件人查看线下转账单且已送达/已完成时返回。 */
+    @JsonInclude(JsonInclude.Include.NON_NULL)
+    private FlashDeliveryRiderBankCardView riderBankCard;
 }

+ 20 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/flashdelivery/dto/FlashDeliveryRiderBankCardView.java

@@ -0,0 +1,20 @@
+package com.ruoyi.app.flashdelivery.dto;
+
+import lombok.Data;
+
+/** 送达后线下转账单展示的骑手启用收款卡;仅暴露转账所需三要素,不携带内部字段。 */
+@Data
+public class FlashDeliveryRiderBankCardView {
+    /** 银行名称(台湾银行字典文案)。 */
+    private String bankName;
+    /** 银行账号。 */
+    private String accountNo;
+    /** 户名。 */
+    private String accountName;
+
+    public FlashDeliveryRiderBankCardView(String bankName, String accountNo, String accountName) {
+        this.bankName = bankName;
+        this.accountNo = accountNo;
+        this.accountName = accountName;
+    }
+}

+ 24 - 14
ruoyi-admin/src/main/java/com/ruoyi/app/flashdelivery/service/FlashDeliveryApplicationService.java

@@ -57,6 +57,8 @@ public class FlashDeliveryApplicationService {
     private static final Set<String> DELIVERY_MODES = Set.of("NOW", "SCHEDULED");
     /** 支付方式取值与主订单系统同域:4=现金、6=线下转账(027 线下转账支付)。 */
     private static final String PAY_TYPE_CASH = "4";
+    /** 线下转账 payType=6:送达后向寄件人展示骑手启用卡,线下转账结清。 */
+    private static final String PAY_TYPE_OFFLINE_TRANSFER = "6";
     private static final String PAY_TYPE_TRANSFER = "6";
     /** 自动取消原因使用稳定代码,供不同端按各自语言展示而不是保存单一语言文本。 */
     private static final String SCHEDULE_EXPIRED_REASON = "SCHEDULE_EXPIRED";
@@ -76,6 +78,7 @@ public class FlashDeliveryApplicationService {
     private final FlashDeliveryOrderImageMapper imageMapper;
     private final FlashDeliveryOrderLogMapper logMapper;
     private final InfoUserMapper userMapper;
+    private final com.ruoyi.system.mapper.InfoBankCardMapper bankCardMapper;
     private final FlashDeliveryRouteService routeService;
     private final FlashDeliveryPricingCalculator pricingCalculator;
     private final RiderDeliveryLockService riderDeliveryLockService;
@@ -89,6 +92,7 @@ public class FlashDeliveryApplicationService {
                                            FlashDeliveryOrderImageMapper imageMapper,
                                            FlashDeliveryOrderLogMapper logMapper,
                                            InfoUserMapper userMapper,
+                                           com.ruoyi.system.mapper.InfoBankCardMapper bankCardMapper,
                                            FlashDeliveryRouteService routeService,
                                            FlashDeliveryPricingCalculator pricingCalculator,
                                            RiderDeliveryLockService riderDeliveryLockService,
@@ -104,6 +108,7 @@ public class FlashDeliveryApplicationService {
         this.imageMapper = imageMapper;
         this.logMapper = logMapper;
         this.userMapper = userMapper;
+        this.bankCardMapper = bankCardMapper;
         this.routeService = routeService;
         this.pricingCalculator = pricingCalculator;
         this.riderDeliveryLockService = riderDeliveryLockService;
@@ -973,6 +978,15 @@ public class FlashDeliveryApplicationService {
         return average == null ? 4.5 : average;
     }
 
+    /** 送达后线下转账单返回骑手启用卡三要素(户名/银行名/账号);无启用卡返回 null。 */
+    private FlashDeliveryRiderBankCardView riderActiveCardView(Long riderId) {
+        if (riderId == null) return null;
+        return bankCardMapper.selectList(currentRead(new QueryWrapper<com.ruoyi.system.domain.InfoBankCard>()
+                        .eq("user_id", riderId).eq("is_active", 1))).stream().findFirst()
+                .map(card -> new FlashDeliveryRiderBankCardView(card.getBankName(), card.getAccountNo(), card.getAccountName()))
+                .orElse(null);
+    }
+
     /** 组装用户或骑手视角详情;hideSensitiveFields 为 true 时按抢单前脱敏返回。 */
     private FlashDeliveryOrderDetailView participantDetail(FlashDeliveryOrder order,
                                                            boolean userView,
@@ -998,6 +1012,11 @@ public class FlashDeliveryApplicationService {
                 }
                 view.setRider(rider);
             }
+            // 线下转账单送达/完成后返回骑手启用卡,供用户线下转账结清(仅寄件人视角)
+            if (PAY_TYPE_OFFLINE_TRANSFER.equals(order.getPayType())
+                    && (DELIVERED.equals(order.getStatus()) || COMPLETED.equals(order.getStatus()))) {
+                view.setRiderBankCard(riderActiveCardView(order.getRiderId()));
+            }
         } else if (!hideSensitiveFields) {
             // 接单后仅中单骑手可见寄件用户完整资料;抢单前脱敏视图不返回用户身份信息。
             if (order.getUserId() != null) {
@@ -1409,23 +1428,14 @@ public class FlashDeliveryApplicationService {
         return query;
     }
 
-    /** 骑手未设置闪送收款方式时继承平台开放项;现金单按既有规则始终可见。 */
+    /** 骑手可抢支付方式:勾选(NULL=平台开放全部)∩ 平台开关 ∩ 就绪度,统一走闸门判定;现金单始终可见。 */
     private Set<String> acceptedRiderPayTypes(InfoUser rider) {
-        String csv = rider.getFlashPayMethods();
-        List<String> groups = new ArrayList<>();
-        if (!hasText(csv)) {
-            for (PaymentMethodGateService.PayMethodOption option : paymentMethodGateService.listAvailable(
-                    PaymentMethodGateService.GateScope.RIDER_FLASH, null, null)) {
-                if (!groups.contains(option.getMethodCode())) groups.add(option.getMethodCode());
-            }
-        } else {
-            for (String part : csv.split(",")) {
-                if (hasText(part)) groups.add(part.trim());
-            }
-        }
         Set<String> payTypes = new LinkedHashSet<>();
         payTypes.add(PAY_TYPE_CASH);
-        for (String group : groups) payTypes.addAll(PaymentMethodGateService.payTypesOf(group));
+        for (PaymentMethodGateService.PayMethodOption option : paymentMethodGateService.listAvailable(
+                PaymentMethodGateService.GateScope.RIDER_FLASH, null, rider.getUserId())) {
+            if (option.isReady()) payTypes.addAll(PaymentMethodGateService.payTypesOf(option.getMethodCode()));
+        }
         return payTypes;
     }
 

+ 2 - 2
ruoyi-admin/src/main/java/com/ruoyi/app/pay/RiderFlashPayMethodController.java

@@ -43,13 +43,13 @@ public class RiderFlashPayMethodController extends BaseController {
         this.userMapper = userMapper;
     }
 
-    /** 设置数据:available=平台闪送维度开放方式;selected=骑手当前选择(NULL 视为全部)。 */
+    /** 设置数据:available=平台闪送维度开放方式(线下转账按骑手启用卡标记就绪度);selected=骑手当前选择(NULL 视为全部)。 */
     @Auth(session = true)
     @GetMapping
     public AjaxResult get(@RequestHeader String token) {
         InfoUser rider = requireRider(token);
         List<PaymentMethodGateService.PayMethodOption> available =
-                gateService.listAvailable(PaymentMethodGateService.GateScope.RIDER_FLASH, null, null);
+                gateService.listAvailable(PaymentMethodGateService.GateScope.RIDER_FLASH, null, rider.getUserId());
         List<String> selected = new ArrayList<>();
         String csv = rider.getFlashPayMethods();
         if (csv != null && !csv.trim().isEmpty()) {

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

@@ -1,2070 +1,2159 @@
-package com.ruoyi.app.flashdelivery.service;
-
-import com.baomidou.mybatisplus.core.conditions.Wrapper;
-import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import com.ruoyi.app.flashdelivery.dto.FlashDeliveryAddressRequest;
-import com.ruoyi.app.flashdelivery.dto.FlashDeliveryAddressChangeRequest;
-import com.ruoyi.app.flashdelivery.dto.FlashDeliveryAddressConfirmRequest;
-import com.ruoyi.app.flashdelivery.dto.FlashDeliveryTipAddRequest;
-import com.ruoyi.app.flashdelivery.dto.FlashDeliveryQuoteView;
-import com.ruoyi.app.flashdelivery.dto.FlashDeliveryCreateRequest;
-import com.ruoyi.app.flashdelivery.dto.FlashDeliveryProofRequest;
-import com.ruoyi.app.flashdelivery.dto.FlashDeliveryPricingRequest;
-import com.ruoyi.app.flashdelivery.dto.FlashDeliveryQuoteRequest;
-import com.ruoyi.app.flashdelivery.dto.FlashDeliveryReasonRequest;
-import com.ruoyi.app.flashdelivery.dto.FlashDeliveryDeliverRequest;
-import com.ruoyi.app.flashdelivery.dto.FlashDeliveryRiderOrderPageView;
-import com.ruoyi.app.flashdelivery.exception.FlashDeliveryQuoteChangedException;
-import com.ruoyi.app.flashdelivery.route.FlashDeliveryRouteService;
-import com.ruoyi.app.flashdelivery.route.GeoPoint;
-import com.ruoyi.app.flashdelivery.route.RouteDistance;
-import com.ruoyi.app.order.RiderDeliveryAcceptanceService;
-import com.ruoyi.app.order.RiderDeliveryLockService;
-import com.ruoyi.system.domain.InfoUser;
-import com.ruoyi.system.domain.flash.FlashDeliveryOrder;
-import com.ruoyi.system.domain.flash.FlashDeliveryOrderImage;
-import com.ruoyi.system.domain.flash.FlashDeliveryPricing;
-import com.ruoyi.system.mapper.InfoUserMapper;
-import com.ruoyi.system.mapper.flash.FlashDeliveryOrderImageMapper;
-import com.ruoyi.system.mapper.flash.FlashDeliveryOrderLogMapper;
-import com.ruoyi.system.mapper.flash.FlashDeliveryOrderMapper;
-import com.ruoyi.system.mapper.flash.FlashDeliveryPricingMapper;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.AfterAll;
-import org.mockito.ArgumentCaptor;
-import org.mockito.InOrder;
-import org.mockito.MockedStatic;
-import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
-import org.springframework.beans.factory.support.DefaultListableBeanFactory;
-import org.springframework.context.support.StaticMessageSource;
-import com.ruoyi.common.core.domain.entity.SysDictData;
-import com.ruoyi.common.utils.DictUtils;
-import com.ruoyi.common.utils.spring.SpringUtils;
-import org.springframework.test.util.ReflectionTestUtils;
-
-import java.math.BigDecimal;
-import java.util.List;
-import java.util.Date;
-import java.util.Calendar;
-
-import static com.ruoyi.system.domain.flash.FlashDeliveryStatus.ACCEPTED;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotEquals;
-import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import com.ruoyi.common.exception.ServiceException;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.isNull;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.mockStatic;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.inOrder;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.verifyNoInteractions;
-
-class FlashDeliveryApplicationServiceTest {
-    private static ConfigurableListableBeanFactory originalBeanFactory;
-
-    @BeforeAll
-    static void initializeMessages() {
-        originalBeanFactory = (ConfigurableListableBeanFactory)
-                ReflectionTestUtils.getField(SpringUtils.class, "beanFactory");
-        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
-        StaticMessageSource messageSource = new StaticMessageSource();
-        messageSource.setUseCodeAsDefaultMessage(true);
-        beanFactory.registerSingleton("messageSource", messageSource);
-        new SpringUtils().postProcessBeanFactory(beanFactory);
-    }
-
-    @AfterAll
-    static void restoreMessages() {
-        new SpringUtils().postProcessBeanFactory(originalBeanFactory);
-    }
-
-    @Test
-    void quoteUsesServerRouteAndCurrentTimePricing() {
-        Fixture fixture = new Fixture();
-        when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
-        when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).thenReturn(new RouteDistance(3001, 600, "ROUTE"));
-
-        var quote = fixture.service.quote(request());
-
-        assertEquals(3001, quote.getDistanceMeters());
-        assertEquals(90L, quote.getAmount());
-        assertEquals(1L, quote.getPricingId());
-        assertEquals("00:00", quote.getStartTime());
-        assertEquals("24:00", quote.getEndTime());
-        assertEquals("ROUTE", quote.getDistanceSource());
-    }
-
-    @Test
-    void quoteRejectsRoutesLongerThanFortyKilometres() {
-        Fixture fixture = new Fixture();
-        when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
-        when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).thenReturn(new RouteDistance(40001, 1800, "ROUTE"));
-
-        assertThrows(ServiceException.class, () -> fixture.service.quote(request()));
-    }
-
-    @Test
-    void wrongDeliveryPinCannotPersistProofOrChangeState() {
-        Fixture fixture = new Fixture();
-        InfoUser rider = new InfoUser();
-        rider.setUserId(8L);
-        rider.setUserType("2");
-        when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
-        FlashDeliveryOrder order = new FlashDeliveryOrder();
-        order.setId(10L);
-        order.setRiderId(8L);
-        order.setStatus("PICKED_UP");
-        order.setPinRequired(true);
-        order.setDeliveryPinCode("4821");
-        when(fixture.orderMapper.selectById(10L)).thenReturn(order);
-        FlashDeliveryDeliverRequest request = new FlashDeliveryDeliverRequest();
-        request.setPinCode("1234");
-        request.setImageUrls(List.of("https://example.com/delivery.jpg"));
-
-        assertThrows(ServiceException.class, () -> fixture.service.deliver(8L, 10L, request));
-        verify(fixture.imageMapper, never()).insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderImage.class));
-        verify(fixture.orderMapper, never()).transitionByRider(any(), any(), any(), any(), any(), any());
-    }
-
-    @Test
-    void correctDeliveryPinPersistsDeliveryProofAndChangesState() {
-        Fixture fixture = new Fixture();
-        InfoUser rider = new InfoUser();
-        rider.setUserId(8L);
-        rider.setUserType("2");
-        when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
-        FlashDeliveryOrder order = new FlashDeliveryOrder();
-        order.setId(10L);
-        order.setRiderId(8L);
-        order.setStatus("PICKED_UP");
-        order.setPinRequired(true);
-        order.setDeliveryPinCode("4821");
-        when(fixture.orderMapper.selectById(10L)).thenReturn(order);
-        when(fixture.orderMapper.transitionByRider(eq(10L), eq(8L), eq("PICKED_UP"),
-                eq("DELIVERED"), eq("delivered_at"), any())).thenReturn(1);
-        FlashDeliveryDeliverRequest request = new FlashDeliveryDeliverRequest();
-        request.setPinCode("4821");
-        request.setImageUrls(List.of("https://example.com/delivery.jpg"));
-
-        fixture.service.deliver(8L, 10L, request);
-
-        ArgumentCaptor<com.ruoyi.system.domain.flash.FlashDeliveryOrderImage> image =
-                ArgumentCaptor.forClass(com.ruoyi.system.domain.flash.FlashDeliveryOrderImage.class);
-        verify(fixture.imageMapper).insert(image.capture());
-        assertEquals("DELIVERY", image.getValue().getProofType());
-        assertEquals("RIDER", image.getValue().getOperatorType());
-    }
-
-    @Test
-    void scheduledCreateGeneratesUserOnlyPinAndStoresRelativeSenderProof() {
-        Fixture fixture = new Fixture();
-        when(fixture.userMapper.selectOrdinaryUserIdsByNormalizedPhone("0922222222"))
-                .thenReturn(List.of(88L));
-        when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
-        when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).thenReturn(new RouteDistance(3200, 600, "ROUTE"));
-        doAnswer(invocation -> {
-            FlashDeliveryOrder order = invocation.getArgument(0);
-            order.setId(99L);
-            return 1;
-        }).when(fixture.orderMapper).insert(any(FlashDeliveryOrder.class));
-        when(fixture.imageMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
-        when(fixture.logMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
-        FlashDeliveryCreateRequest request = createRequest();
-        request.setSenderImageUrls(List.of("/profile/upload/2026/09/07/sender.jpg"));
-
-        var result = fixture.service.create(7L, request);
-
-        assertEquals(4, result.getDeliveryPinCode().length());
-        ArgumentCaptor<com.ruoyi.system.domain.flash.FlashDeliveryOrderImage> image =
-                ArgumentCaptor.forClass(com.ruoyi.system.domain.flash.FlashDeliveryOrderImage.class);
-        verify(fixture.imageMapper).insert(image.capture());
-        assertEquals("SENDER", image.getValue().getProofType());
-        assertEquals("USER", image.getValue().getOperatorType());
-        assertEquals("/profile/upload/2026/09/07/sender.jpg", image.getValue().getImageUrl());
-        ArgumentCaptor<FlashDeliveryOrder> saved = ArgumentCaptor.forClass(FlashDeliveryOrder.class);
-        verify(fixture.orderMapper).insert(saved.capture());
-        assertEquals("SCHEDULED", saved.getValue().getDeliveryMode());
-        assertEquals("NORMAL", saved.getValue().getDeliveryType());
-        assertEquals("DOCUMENT", saved.getValue().getPackageType());
-        assertEquals(1, saved.getValue().getQuantity());
-        assertEquals("UP_TO_5_KG", saved.getValue().getWeightRange());
-        assertEquals("30 x 20 x 10 cm", saved.getValue().getSpecification());
-        assertEquals(88L, saved.getValue().getReceiverUserId());
-        assertEquals(1L, saved.getValue().getPricingId());
-        assertEquals("00:00", saved.getValue().getPricingStartTime());
-        assertEquals("24:00", saved.getValue().getPricingEndTime());
-        assertEquals(new BigDecimal("3.00"), saved.getValue().getStartingDistance());
-        assertEquals(90L, saved.getValue().getStartingFare());
-        assertEquals(new BigDecimal("1.00"), saved.getValue().getDistance());
-        assertEquals(15L, saved.getValue().getFreight());
-        assertEquals(0L, saved.getValue().getDistanceFee());
-        assertEquals(90L, saved.getValue().getBaseDeliveryFee());
-        assertEquals(new BigDecimal("20.00"), saved.getValue().getUrgentRate());
-        assertEquals(10L, saved.getValue().getMinimumUrgentFee());
-        assertEquals(0L, saved.getValue().getUrgentFee());
-        assertEquals(0L, saved.getValue().getTipAmount());
-    }
-
-    @Test
-    void confirmPaymentRequiresDeliveredState() {
-        Fixture fixture = new Fixture();
-        rider(fixture, 9L);
-        FlashDeliveryOrder pickedUp = order(10L, "PICKED_UP");
-        pickedUp.setRiderId(9L);
-        when(fixture.orderMapper.selectById(10L)).thenReturn(pickedUp);
-
-        assertThrows(ServiceException.class, () -> fixture.service.confirmPayment(9L, 10L));
-        verify(fixture.orderMapper, never()).update(any(), any());
-    }
-
-    @Test
-    void confirmPaymentMarksPaidAndWritesLog() {
-        Fixture fixture = new Fixture();
-        rider(fixture, 9L);
-        FlashDeliveryOrder delivered = order(10L, "DELIVERED");
-        delivered.setRiderId(9L);
-        when(fixture.orderMapper.selectById(10L)).thenReturn(delivered);
-        when(fixture.orderMapper.update(any(), any())).thenReturn(1);
-        when(fixture.logMapper.insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderLog.class))).thenReturn(1);
-
-        fixture.service.confirmPayment(9L, 10L);
-
-        verify(fixture.orderMapper).update(any(), any());
-        ArgumentCaptor<com.ruoyi.system.domain.flash.FlashDeliveryOrderLog> logCaptor =
-                ArgumentCaptor.forClass(com.ruoyi.system.domain.flash.FlashDeliveryOrderLog.class);
-        verify(fixture.logMapper).insert(logCaptor.capture());
-        assertEquals("PAYMENT_CONFIRMED", logCaptor.getValue().getReason());
-    }
-
-    @Test
-    void confirmPaymentIsIdempotentOnceConfirmed() {
-        Fixture fixture = new Fixture();
-        rider(fixture, 9L);
-        FlashDeliveryOrder confirmed = order(10L, "DELIVERED");
-        confirmed.setRiderId(9L);
-        confirmed.setPaymentStatus(1);
-        when(fixture.orderMapper.selectById(10L)).thenReturn(confirmed);
-
-        fixture.service.confirmPayment(9L, 10L);
-
-        verify(fixture.orderMapper, never()).update(any(), any());
-        verify(fixture.logMapper, never()).insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderLog.class));
-    }
-
-    @Test
-    @SuppressWarnings({"rawtypes", "unchecked"})
-    void userListOnlyFiltersByOwnerAndReturnsCardFields() {
-        Fixture fixture = new Fixture();
-        FlashDeliveryOrder order = order(10L, "COMPLETED");
-        order.setWeightRange("OVER_5_TO_10_KG");
-        Page<FlashDeliveryOrder> source = new Page<>(1, 10, 1);
-        source.setRecords(List.of(order));
-        when(fixture.orderMapper.selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
-                any(Wrapper.class))).thenReturn(source);
-
-        var result = fixture.service.userOrders(7L, 1, 10, null);
-
-        ArgumentCaptor<com.baomidou.mybatisplus.core.conditions.query.QueryWrapper> query =
-                ArgumentCaptor.forClass(com.baomidou.mybatisplus.core.conditions.query.QueryWrapper.class);
-        verify(fixture.orderMapper).selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class), query.capture());
-        String sql = query.getValue().getSqlSegment().toLowerCase();
-        var parameters = query.getValue().getParamNameValuePairs().values();
-        assertTrue(sql.contains("user_id"));
-        assertFalse(sql.contains("status"));
-        assertFalse(sql.contains("service_type"));
-        assertTrue(parameters.contains(7L));
-        assertEquals("pickup road", result.getRecords().get(0).getPickupAddress());
-        assertEquals("OVER_5_TO_10_KG", result.getRecords().get(0).getWeightRange());
-        assertEquals(112L, result.getRecords().get(0).getAmount());
-    }
-
-    @Test
-    void createDoesNotBindReceiverWhenNormalizedPhoneMatchesMultipleUsers() {
-        Fixture fixture = new Fixture();
-        when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
-        when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).thenReturn(new RouteDistance(3200, 600, "ROUTE"));
-        when(fixture.userMapper.selectOrdinaryUserIdsByNormalizedPhone("886912345678"))
-                .thenReturn(List.of(88L, 99L));
-        doAnswer(invocation -> {
-            FlashDeliveryOrder order = invocation.getArgument(0);
-            order.setId(99L);
-            return 1;
-        }).when(fixture.orderMapper).insert(any(FlashDeliveryOrder.class));
-        FlashDeliveryCreateRequest request = createRequest();
-        request.getDelivery().setPhone("+886 912-345-678");
-
-        fixture.service.create(7L, request);
-
-        ArgumentCaptor<FlashDeliveryOrder> saved = ArgumentCaptor.forClass(FlashDeliveryOrder.class);
-        verify(fixture.orderMapper).insert(saved.capture());
-        assertNull(saved.getValue().getReceiverUserId());
-    }
-
-    @Test
-    void createLeavesReceiverUnboundWhenPhoneHasNoRegisteredUser() {
-        Fixture fixture = new Fixture();
-        when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
-        when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).thenReturn(new RouteDistance(3200, 600, "ROUTE"));
-        when(fixture.userMapper.selectOrdinaryUserIdsByNormalizedPhone("0922222222"))
-                .thenReturn(List.of());
-        doAnswer(invocation -> {
-            FlashDeliveryOrder order = invocation.getArgument(0);
-            order.setId(99L);
-            return 1;
-        }).when(fixture.orderMapper).insert(any(FlashDeliveryOrder.class));
-
-        fixture.service.create(7L, createRequest());
-
-        ArgumentCaptor<FlashDeliveryOrder> saved = ArgumentCaptor.forClass(FlashDeliveryOrder.class);
-        verify(fixture.orderMapper).insert(saved.capture());
-        assertNull(saved.getValue().getReceiverUserId());
-    }
-
-    @Test
-    @SuppressWarnings({"rawtypes", "unchecked"})
-    void receiverListFiltersByCreationTimeReceiverBinding() {
-        Fixture fixture = new Fixture();
-        when(fixture.orderMapper.selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
-                any(Wrapper.class))).thenReturn(new Page<FlashDeliveryOrder>());
-
-        fixture.service.userOrders(88L, 1, 10, "receiver");
-
-        ArgumentCaptor<com.baomidou.mybatisplus.core.conditions.query.QueryWrapper> query =
-                ArgumentCaptor.forClass(com.baomidou.mybatisplus.core.conditions.query.QueryWrapper.class);
-        verify(fixture.orderMapper).selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class), query.capture());
-        assertTrue(query.getValue().getSqlSegment().toLowerCase().contains("receiver_user_id"));
-        assertTrue(query.getValue().getParamNameValuePairs().values().contains(88L));
-    }
-
-    @Test
-    @SuppressWarnings({"rawtypes", "unchecked"})
-    void riderNewTaskUsesFoodOrderParametersAndReturnsPrototypeCardSummary() {
-        Fixture fixture = new Fixture();
-        rider(fixture, 8L);
-        FlashDeliveryOrder order = order(10L, "WAITING_ACCEPTANCE");
-        order.setWeightRange("OVER_10_TO_15_KG");
-        Page<FlashDeliveryOrder> source = new Page<>(1, 10, 1);
-        source.setRecords(List.of(order));
-        when(fixture.orderMapper.selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
-                any(Wrapper.class))).thenReturn(source);
-        when(fixture.orderMapper.selectObjs(any(Wrapper.class))).thenReturn(List.of(168L));
-
-        SysDictData distanceLimit = new SysDictData();
-        distanceLimit.setDictValue("5");
-        FlashDeliveryRiderOrderPageView result;
-        try (MockedStatic<DictUtils> dictionary = mockStatic(DictUtils.class)) {
-            dictionary.when(() -> DictUtils.getDictCache("sys_qs_newtask_distance"))
-                    .thenReturn(List.of(distanceLimit));
-            result = fixture.service.riderOrders(8L, 1, 10, "newTask",
-                    new BigDecimal("121.5000"), new BigDecimal("25.0000"));
-        }
-
-        assertEquals(1L, result.getNearbyTaskCount());
-        assertEquals(168L, result.getHighestOrderAmount());
-        assertEquals("pickup road", result.getRecords().get(0).getPickupAddress());
-        assertEquals("delivery road", result.getRecords().get(0).getDeliveryAddress());
-        assertEquals("OVER_10_TO_15_KG", result.getRecords().get(0).getWeightRange());
-        assertTrue(result.getRecords().get(0).getPickupDistanceMeters() > 0);
-        assertTrue(result.getRecords().get(0).getPinRequired());
-        ArgumentCaptor<com.baomidou.mybatisplus.core.conditions.query.QueryWrapper> query =
-                ArgumentCaptor.forClass(com.baomidou.mybatisplus.core.conditions.query.QueryWrapper.class);
-        verify(fixture.orderMapper).selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class), query.capture());
-        String sql = query.getValue().getSqlSegment().toLowerCase();
-        var parameters = query.getValue().getParamNameValuePairs().values();
-        assertTrue(sql.contains("status"));
-        assertTrue(sql.contains("rider_id is null"));
-        assertTrue(sql.contains("is_display"));
-        assertTrue(sql.contains("vehicle_type"));
-        assertTrue(sql.contains("scheduled_pickup_start_at"));
-        assertTrue(sql.contains("scheduled_pickup_end_at"));
-        assertTrue(sql.contains("st_distance_sphere"));
-        assertTrue(parameters.contains("WAITING_ACCEPTANCE"));
-        assertTrue(parameters.contains("NOW"));
-        assertTrue(parameters.stream().anyMatch(Date.class::isInstance));
-
-        ArgumentCaptor<com.baomidou.mybatisplus.core.conditions.query.QueryWrapper> summaryQuery =
-                ArgumentCaptor.forClass(com.baomidou.mybatisplus.core.conditions.query.QueryWrapper.class);
-        verify(fixture.orderMapper).selectObjs(summaryQuery.capture());
-        String summarySql = summaryQuery.getValue().getSqlSegment().toLowerCase();
-        var summaryParameters = summaryQuery.getValue().getParamNameValuePairs().values();
-        assertTrue(summarySql.contains("rider_id is null"));
-        assertTrue(summarySql.contains("scheduled_pickup_start_at"));
-        assertTrue(summarySql.contains("scheduled_pickup_end_at"));
-        assertTrue(summarySql.contains("st_distance_sphere"));
-        assertTrue(summaryParameters.contains("WAITING_ACCEPTANCE"));
-    }
-
-    @Test
-    @SuppressWarnings({"rawtypes", "unchecked"})
-    void riderNewTaskWithoutPositionFallsBackToNewestFirst() {
-        Fixture fixture = new Fixture();
-        rider(fixture, 8L);
-        when(fixture.orderMapper.selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
-                any(Wrapper.class))).thenReturn(new Page<FlashDeliveryOrder>());
-        when(fixture.orderMapper.selectObjs(any(Wrapper.class))).thenReturn(List.of());
-
-        fixture.service.riderOrders(8L, 1, 10, "newTask", null, null);
-
-        ArgumentCaptor<com.baomidou.mybatisplus.core.conditions.query.QueryWrapper> query =
-                ArgumentCaptor.forClass(com.baomidou.mybatisplus.core.conditions.query.QueryWrapper.class);
-        verify(fixture.orderMapper).selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class), query.capture());
-        String sql = query.getValue().getSqlSegment().toLowerCase();
-        assertTrue(sql.contains("order by create_time desc"));
-        assertFalse(sql.contains("st_distance_sphere"));
-    }
-
-    @Test
-    @SuppressWarnings({"rawtypes", "unchecked"})
-    void riderTabsMapStatusesBeforePagingAndRestrictAssignedTabsToRider() {
-        assertRiderTab("toPickup", List.of("ACCEPTED"));
-        assertRiderTab("delivering", List.of("PICKED_UP"));
-        assertRiderTab("completed", List.of("DELIVERED", "COMPLETED"));
-        assertRiderTab("cancelled", List.of("CANCELLED"));
-    }
-
-    @Test
-    void riderListRejectsUnknownTabAndUnpairedOrInvalidCoordinates() {
-        Fixture fixture = new Fixture();
-        rider(fixture, 8L);
-
-        assertThrows(ServiceException.class,
-                () -> fixture.service.riderOrders(8L, 1, 10, "refund", null, null));
-        assertThrows(ServiceException.class,
-                () -> fixture.service.riderOrders(8L, 1, 10, null, null, null));
-        assertThrows(ServiceException.class,
-                () -> fixture.service.riderOrders(8L, 1, 10, "newTask", BigDecimal.ONE, null));
-        assertThrows(ServiceException.class,
-                () -> fixture.service.riderOrders(8L, 1, 10, "newTask",
-                        new BigDecimal("181"), BigDecimal.ZERO));
-        verify(fixture.orderMapper, never()).selectPage(any(), any());
-    }
-
-    @Test
-    void flashNewTaskListAndAcceptRequireFlashDeliveryType() {
-        Fixture fixture = new Fixture();
-        InfoUser rider = new InfoUser();
-        rider.setUserId(8L);
-        rider.setUserType("2");
-        rider.setDeliveryType("FOOD");
-        when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
-
-        assertThrows(ServiceException.class,
-                () -> fixture.service.riderOrders(8L, 1, 10, "newTask", null, null));
-        assertThrows(ServiceException.class, () -> fixture.service.accept(8L, 10L));
-        verify(fixture.orderMapper, never()).selectPage(any(), any());
-        verify(fixture.orderMapper, never()).accept(any(), any(), any());
-    }
-
-    @Test
-    void riderDetailHidesWaitingFlashOrdersFromNonFlashRiders() {
-        Fixture fixture = new Fixture();
-        InfoUser rider = new InfoUser();
-        rider.setUserId(8L);
-        rider.setUserType("2");
-        rider.setDeliveryType("FOOD");
-        when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
-        when(fixture.orderMapper.selectById(10L)).thenReturn(order(10L, "WAITING_ACCEPTANCE"));
-
-        assertThrows(ServiceException.class, () -> fixture.service.riderDetail(8L, 10L));
-        verify(fixture.imageMapper, never()).selectList(any());
-    }
-
-    @Test
-    @SuppressWarnings({"rawtypes", "unchecked"})
-    void enabledFlashRiderStillListsNewTasks() {
-        Fixture fixture = new Fixture();
-        InfoUser rider = new InfoUser();
-        rider.setUserId(8L);
-        rider.setUserType("2");
-        rider.setDeliveryType("FLASH");
-        when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
-        when(fixture.orderMapper.selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
-                any(Wrapper.class))).thenReturn(new Page<FlashDeliveryOrder>());
-
-        fixture.service.riderOrders(8L, 1, 10, "newTask", null, null);
-        verify(fixture.orderMapper).selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
-                any(Wrapper.class));
-    }
-
-    @Test
-    @SuppressWarnings({"rawtypes", "unchecked"})
-    void ownOrderTabsRemainAccessibleRegardlessOfDeliveryType() {
-        Fixture fixture = new Fixture();
-        InfoUser rider = new InfoUser();
-        rider.setUserId(8L);
-        rider.setUserType("2");
-        rider.setDeliveryType("FOOD");
-        when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
-        when(fixture.orderMapper.selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
-                any(Wrapper.class))).thenReturn(new Page<FlashDeliveryOrder>());
-
-        fixture.service.riderOrders(8L, 1, 10, "toPickup", null, null);
-        verify(fixture.orderMapper).selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
-                any(Wrapper.class));
-    }
-
-    @Test
-    void assignedRiderTabsIgnoreLocationParameters() {
-        Fixture fixture = new Fixture();
-        rider(fixture, 8L);
-        when(fixture.orderMapper.selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
-                any(Wrapper.class))).thenReturn(new Page<FlashDeliveryOrder>());
-
-        fixture.service.riderOrders(8L, 1, 10, "toPickup",
-                new BigDecimal("181"), new BigDecimal("91"));
-
-        verify(fixture.orderMapper).selectPage(any(), any());
-    }
-
-    @Test
-    void preAcceptRiderDetailIsDirectAndShowsTextAddressWithoutSensitiveFields() {
-        Fixture fixture = new Fixture();
-        rider(fixture, 8L);
-        FlashDeliveryOrder order = order(10L, "WAITING_ACCEPTANCE");
-        order.setWeightRange("OVER_15_TO_20_KG");
-        when(fixture.orderMapper.selectById(10L)).thenReturn(order);
-
-        var result = fixture.service.riderDetail(8L, 10L);
-
-        assertEquals(10L, result.getId());
-        assertEquals("pickup road", result.getPickup().getAddress());
-        assertEquals("pickup door", result.getPickup().getAddressDetail());
-        assertEquals("delivery road", result.getDelivery().getAddress());
-        assertEquals("delivery door", result.getDelivery().getAddressDetail());
-        assertEquals("OVER_15_TO_20_KG", result.getWeightRange());
-        assertNull(result.getPickup().getName());
-        assertNull(result.getPickup().getPhone());
-        assertNull(result.getPickup().getLongitude());
-        assertNull(result.getPickup().getLatitude());
-        assertNull(result.getDelivery().getName());
-        assertNull(result.getDelivery().getPhone());
-        assertNull(result.getDelivery().getLongitude());
-        assertNull(result.getDelivery().getLatitude());
-        assertNull(result.getDeliveryPinCode());
-        assertNull(result.getUserNote());
-        assertNull(result.getUser());
-        assertTrue(result.getSenderImageUrls().isEmpty());
-        assertTrue(result.getPickupImageUrls().isEmpty());
-        assertTrue(result.getDeliveryImageUrls().isEmpty());
-        verify(fixture.userMapper, never()).selectInfoUserByUserId(7L);
-        verify(fixture.imageMapper, never()).selectList(any());
-        verify(fixture.logMapper, never()).selectList(any());
-    }
-
-    @Test
-    void userDetailReturnsDirectOrderAndGroupsImageUrlsWithoutRawLogs() {
-        Fixture fixture = new Fixture();
-        FlashDeliveryOrder order = order(10L, ACCEPTED);
-        order.setRiderId(null);
-        order.setWeightRange("OVER_5_TO_10_KG");
-        when(fixture.orderMapper.selectById(10L)).thenReturn(order);
-        when(fixture.imageMapper.selectList(any(Wrapper.class))).thenReturn(List.of(
-                image("SENDER", "https://example.com/sender.jpg"),
-                image("PICKUP", "https://example.com/pickup.jpg"),
-                image("DELIVERY", "https://example.com/delivery.jpg")));
-
-        var result = fixture.service.userDetail(7L, 10L);
-
-        assertEquals(10L, result.getId());
-        assertEquals(List.of("https://example.com/sender.jpg"), result.getSenderImageUrls());
-        assertEquals(List.of("https://example.com/pickup.jpg"), result.getPickupImageUrls());
-        assertEquals(List.of("https://example.com/delivery.jpg"), result.getDeliveryImageUrls());
-        assertEquals("4821", result.getDeliveryPinCode());
-        assertEquals("OVER_5_TO_10_KG", result.getWeightRange());
-        verify(fixture.logMapper, never()).selectList(any());
-    }
-
-    @Test
-    void userDetailReturnsRiderInfoAfterAcceptance() {
-        Fixture fixture = new Fixture();
-        FlashDeliveryOrder order = order(10L, ACCEPTED);
-        order.setRiderId(8L);
-        when(fixture.orderMapper.selectById(10L)).thenReturn(order);
-        InfoUser rider = new InfoUser();
-        rider.setUserId(8L);
-        rider.setUserType("2");
-        rider.setNickName("闪送骑士");
-        rider.setTelPhone("0933333333");
-        rider.setVehicleType("2");
-        rider.setLicensePlate("ABC-1234");
-        when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
-
-        var result = fixture.service.userDetail(7L, 10L);
-
-        assertEquals(8L, result.getRider().getUserId());
-        assertEquals("闪送骑士", result.getRider().getNickName());
-        assertEquals("0933333333", result.getRider().getTelPhone());
-        assertEquals("ABC-1234", result.getRider().getLicensePlate());
-        assertNull(result.getUser());
-    }
-
-    @Test
-    void assignedRiderDetailReturnsSenderInfo() {
-        Fixture fixture = new Fixture();
-        rider(fixture, 8L);
-        FlashDeliveryOrder order = order(10L, ACCEPTED);
-        order.setRiderId(8L);
-        when(fixture.orderMapper.selectById(10L)).thenReturn(order);
-        InfoUser sender = new InfoUser();
-        sender.setUserId(7L);
-        sender.setNickName("寄件达人");
-        sender.setPhone("0911111111");
-        sender.setAvatar("https://example.com/avatar.png");
-        when(fixture.userMapper.selectInfoUserByUserId(7L)).thenReturn(sender);
-
-        var result = fixture.service.riderDetail(8L, 10L);
-
-        assertEquals(7L, result.getUser().getUserId());
-        assertEquals("寄件达人", result.getUser().getNickName());
-        assertEquals("0911111111", result.getUser().getPhone());
-        assertEquals("https://example.com/avatar.png", result.getUser().getAvatar());
-        assertNull(result.getRider());
-    }
-
-    @Test
-    void matchedReceiverCanViewAndConfirmButCannotCancelOrder() {
-        Fixture fixture = new Fixture();
-        FlashDeliveryOrder order = order(10L, "DELIVERED");
-        order.setReceiverUserId(88L);
-        when(fixture.orderMapper.selectById(10L)).thenReturn(order);
-        when(fixture.orderMapper.transitionByParticipant(eq(10L), eq(88L), eq("DELIVERED"),
-                eq("COMPLETED"), any(Date.class))).thenReturn(1);
-        when(fixture.imageMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
-
-        assertDoesNotThrow(() -> fixture.service.userDetail(88L, 10L));
-        assertDoesNotThrow(() -> fixture.service.confirmReceipt(88L, 10L));
-        assertThrows(ServiceException.class,
-                () -> fixture.service.userCancel(88L, 10L, new com.ruoyi.app.flashdelivery.dto.FlashDeliveryReasonRequest()));
-        verify(fixture.orderMapper).transitionByParticipant(eq(10L), eq(88L), eq("DELIVERED"),
-                eq("COMPLETED"), any(Date.class));
-    }
-
-    @Test
-    void senderRatesRiderOnCompletedOrder() {
-        Fixture fixture = new Fixture();
-        FlashDeliveryOrder order = order(10L, "COMPLETED");
-        order.setRiderId(8L);
-        when(fixture.orderMapper.selectById(10L)).thenReturn(order);
-        when(fixture.orderMapper.rateRider(eq(10L), eq(7L), eq(4.5), any(Date.class))).thenReturn(1);
-
-        fixture.service.rateRider(7L, 10L, rating(4.5));
-
-        verify(fixture.orderMapper).rateRider(eq(10L), eq(7L), eq(4.5), any(Date.class));
-    }
-
-    @Test
-    void rateRiderRejectsUserOtherThanSender() {
-        Fixture fixture = new Fixture();
-        FlashDeliveryOrder order = order(10L, "COMPLETED");
-        order.setRiderId(8L);
-        when(fixture.orderMapper.selectById(10L)).thenReturn(order);
-
-        assertThrows(ServiceException.class, () -> fixture.service.rateRider(88L, 10L, rating(4.5)));
-        verify(fixture.orderMapper, never()).rateRider(any(), any(), any(), any());
-    }
-
-    @Test
-    void rateRiderRejectsOrderNotCompletedOrWithoutRider() {
-        Fixture fixture = new Fixture();
-        FlashDeliveryOrder delivered = order(10L, "DELIVERED");
-        delivered.setRiderId(8L);
-        when(fixture.orderMapper.selectById(10L)).thenReturn(delivered);
-        assertThrows(ServiceException.class, () -> fixture.service.rateRider(7L, 10L, rating(4.5)));
-
-        FlashDeliveryOrder completedWithoutRider = order(11L, "COMPLETED");
-        when(fixture.orderMapper.selectById(11L)).thenReturn(completedWithoutRider);
-        assertThrows(ServiceException.class, () -> fixture.service.rateRider(7L, 11L, rating(4.5)));
-
-        verify(fixture.orderMapper, never()).rateRider(any(), any(), any(), any());
-    }
-
-    @Test
-    void rateRiderRejectsAlreadyRatedOrder() {
-        Fixture fixture = new Fixture();
-        FlashDeliveryOrder order = order(10L, "COMPLETED");
-        order.setRiderId(8L);
-        order.setRiderStars(4.5);
-        when(fixture.orderMapper.selectById(10L)).thenReturn(order);
-
-        assertThrows(ServiceException.class, () -> fixture.service.rateRider(7L, 10L, rating(3.0)));
-        verify(fixture.orderMapper, never()).rateRider(any(), any(), any(), any());
-    }
-
-    @Test
-    void rateRiderRejectsConcurrentDuplicateSubmission() {
-        Fixture fixture = new Fixture();
-        FlashDeliveryOrder order = order(10L, "COMPLETED");
-        order.setRiderId(8L);
-        when(fixture.orderMapper.selectById(10L)).thenReturn(order);
-        when(fixture.orderMapper.rateRider(eq(10L), eq(7L), eq(4.5), any(Date.class))).thenReturn(0);
-
-        assertThrows(ServiceException.class, () -> fixture.service.rateRider(7L, 10L, rating(4.5)));
-    }
-
-    @Test
-    void rateRiderRejectsMissingOrOutOfRangeStars() {
-        Fixture fixture = new Fixture();
-
-        assertThrows(ServiceException.class, () -> fixture.service.rateRider(7L, 10L, null));
-        assertThrows(ServiceException.class, () -> fixture.service.rateRider(7L, 10L, rating(null)));
-        assertThrows(ServiceException.class, () -> fixture.service.rateRider(7L, 10L, rating(0.5)));
-        assertThrows(ServiceException.class, () -> fixture.service.rateRider(7L, 10L, rating(5.5)));
-
-        verify(fixture.orderMapper, never()).selectById(any());
-        verify(fixture.orderMapper, never()).rateRider(any(), any(), any(), any());
-    }
-
-    @Test
-    void userDetailReturnsRiderStars() {
-        Fixture fixture = new Fixture();
-        FlashDeliveryOrder order = order(10L, "COMPLETED");
-        order.setRiderId(8L);
-        order.setRiderStars(4.5);
-        when(fixture.orderMapper.selectById(10L)).thenReturn(order);
-        when(fixture.imageMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
-
-        var view = fixture.service.userDetail(7L, 10L);
-
-        assertEquals(4.5, view.getRiderStars());
-    }
-
-    @Test
-    void userDetailRiderCarriesFlashAverageStars() {
-        Fixture fixture = new Fixture();
-        FlashDeliveryOrder order = order(10L, "ACCEPTED");
-        order.setRiderId(8L);
-        when(fixture.orderMapper.selectById(10L)).thenReturn(order);
-        InfoUser rider = new InfoUser();
-        rider.setUserId(8L);
-        rider.setNickName("闪送骑手");
-        when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
-        when(fixture.orderMapper.selectRiderAverageStars(8L)).thenReturn(4.8);
-        when(fixture.imageMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
-
-        var view = fixture.service.userDetail(7L, 10L);
-
-        assertEquals(4.8, view.getRider().getStar());
-    }
-
-    @Test
-    void userDetailRiderFallsBackToDefaultStarsWithoutRatings() {
-        Fixture fixture = new Fixture();
-        FlashDeliveryOrder order = order(10L, "ACCEPTED");
-        order.setRiderId(8L);
-        when(fixture.orderMapper.selectById(10L)).thenReturn(order);
-        InfoUser rider = new InfoUser();
-        rider.setUserId(8L);
-        when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
-        when(fixture.orderMapper.selectRiderAverageStars(8L)).thenReturn(null);
-        when(fixture.imageMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
-
-        var view = fixture.service.userDetail(7L, 10L);
-
-        assertEquals(4.5, view.getRider().getStar());
-    }
-
-    @Test
-    void userDetailByOrderNoReturnsParticipantDetail() {
-        Fixture fixture = new Fixture();
-        FlashDeliveryOrder order = order(10L, "COMPLETED");
-        order.setRiderId(8L);
-        order.setRiderStars(4.5);
-        when(fixture.orderMapper.selectOne(any(Wrapper.class))).thenReturn(order);
-        InfoUser rider = new InfoUser();
-        rider.setUserId(8L);
-        when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
-        when(fixture.imageMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
-
-        var view = fixture.service.userDetailByOrderNo(7L, "FL-20260813-021");
-
-        assertEquals(10L, view.getId());
-        assertEquals(4.5, view.getRiderStars());
-    }
-
-    @Test
-    void userDetailByOrderNoRejectsNonParticipantAndUnknownOrderNo() {
-        Fixture fixture = new Fixture();
-        FlashDeliveryOrder order = order(10L, "COMPLETED");
-        order.setReceiverUserId(null);
-        when(fixture.orderMapper.selectOne(any(Wrapper.class))).thenReturn(order);
-        assertThrows(ServiceException.class,
-                () -> fixture.service.userDetailByOrderNo(88L, "FL-20260813-021"));
-
-        when(fixture.orderMapper.selectOne(any(Wrapper.class))).thenReturn(null);
-        assertThrows(ServiceException.class,
-                () -> fixture.service.userDetailByOrderNo(7L, "FL-UNKNOWN"));
-    }
-
-    @Test
-    void riderDetailByOrderNoReturnsAssignedOrderDetail() {
-        Fixture fixture = new Fixture();
-        rider(fixture, 8L);
-        FlashDeliveryOrder order = order(10L, "ACCEPTED");
-        order.setRiderId(8L);
-        when(fixture.orderMapper.selectOne(any(Wrapper.class))).thenReturn(order);
-        when(fixture.userMapper.selectInfoUserByUserId(7L)).thenReturn(new InfoUser());
-        when(fixture.imageMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
-
-        var view = fixture.service.riderDetailByOrderNo(8L, "FL-20260813-021");
-
-        assertEquals(10L, view.getId());
-        assertNotNull(view.getUser());
-    }
-
-    @Test
-    void riderDetailByOrderNoRejectsUnknownOrderNo() {
-        Fixture fixture = new Fixture();
-        rider(fixture, 8L);
-        when(fixture.orderMapper.selectOne(any(Wrapper.class))).thenReturn(null);
-
-        assertThrows(ServiceException.class,
-                () -> fixture.service.riderDetailByOrderNo(8L, "FL-UNKNOWN"));
-    }
-
-    @Test
-    void orderWithoutCreationTimeReceiverBindingIsNotClaimedLater() {
-        Fixture fixture = new Fixture();
-        FlashDeliveryOrder historicalOrder = order(10L, "DELIVERED");
-        historicalOrder.setReceiverUserId(null);
-        when(fixture.orderMapper.selectById(10L)).thenReturn(historicalOrder);
-
-        assertThrows(ServiceException.class, () -> fixture.service.userDetail(88L, 10L));
-
-        verify(fixture.userMapper, never()).selectOrdinaryUserIdsByNormalizedPhone(any());
-    }
-
-    @Test
-    void pickupRejectsMissingProofBeforeChangingState() {
-        Fixture fixture = new Fixture();
-        InfoUser rider = new InfoUser();
-        rider.setUserId(8L);
-        rider.setUserType("2");
-        when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
-        FlashDeliveryProofRequest request = new FlashDeliveryProofRequest();
-        request.setImageUrls(List.of());
-
-        assertThrows(RuntimeException.class, () -> fixture.service.pickup(8L, 10L, request));
-        verify(fixture.orderMapper, never()).transitionByRider(any(), any(), any(), any(), any(), any());
-    }
-
-    @Test
-    void atomicAcceptFailureDoesNotBindSecondRider() {
-        Fixture fixture = new Fixture();
-        InfoUser rider = new InfoUser();
-        rider.setUserId(8L);
-        rider.setUserType("2");
-        rider.setDeliveryType("FLASH");
-        when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
-        when(fixture.orderMapper.selectById(10L)).thenReturn(order(10L, "WAITING_ACCEPTANCE"));
-        when(fixture.orderMapper.accept(eq(10L), eq(8L), any())).thenReturn(0);
-
-        assertThrows(RuntimeException.class, () -> fixture.service.accept(8L, 10L));
-    }
-
-    // 预约单已过取件结束时间:即使骑手绕过列表直接持有订单 ID 请求接单,锁内校验也必须拒绝。
-    @Test
-    void expiredScheduledOrderCannotBeAcceptedEvenWhenItsIdIsKnown() {
-        Fixture fixture = new Fixture();
-        rider(fixture, 8L);
-        FlashDeliveryOrder order = order(10L, "WAITING_ACCEPTANCE");
-        Date now = new Date();
-        order.setDeliveryMode("SCHEDULED");
-        order.setScheduledPickupStartAt(new Date(now.getTime() - 31L * 60L * 1000L));
-        order.setScheduledPickupEndAt(new Date(now.getTime() - 60L * 1000L));
-        when(fixture.orderMapper.selectById(10L)).thenReturn(order);
-
-        assertThrows(ServiceException.class, () -> fixture.service.accept(8L, 10L));
-        verify(fixture.orderMapper, never()).accept(any(), any(), any());
-    }
-
-    @Test
-    void flashAcceptUsesRiderLockAndChecksAcceptancePolicyBeforeAtomicUpdate() {
-        Fixture fixture = new Fixture();
-        rider(fixture, 8L);
-        FlashDeliveryOrder waiting = order(10L, "WAITING_ACCEPTANCE");
-        FlashDeliveryOrder accepted = order(10L, ACCEPTED);
-        accepted.setRiderId(8L);
-        when(fixture.orderMapper.selectById(10L)).thenReturn(waiting, accepted);
-        when(fixture.orderMapper.accept(eq(10L), eq(8L), any(Date.class))).thenReturn(1);
-
-        fixture.service.accept(8L, 10L);
-
-        InOrder order = inOrder(fixture.lockService, fixture.acceptanceService, fixture.orderMapper);
-        order.verify(fixture.lockService).withLock(eq(8L), any(RiderDeliveryLockService.LockedCall.class));
-        order.verify(fixture.acceptanceService).assertCanAcceptFlash(8L, "NORMAL");
-        order.verify(fixture.orderMapper).accept(eq(10L), eq(8L), any(Date.class));
-    }
-
-    @Test
-    void flashAcceptanceConflictDoesNotUpdateOrder() {
-        Fixture fixture = new Fixture();
-        rider(fixture, 8L);
-        when(fixture.orderMapper.selectById(10L)).thenReturn(order(10L, "WAITING_ACCEPTANCE"));
-        doThrow(new ServiceException("exclusive"))
-                .when(fixture.acceptanceService).assertCanAcceptFlash(8L, "NORMAL");
-
-        assertThrows(ServiceException.class, () -> fixture.service.accept(8L, 10L));
-
-        verify(fixture.orderMapper, never()).accept(any(), any(), any());
-    }
-
-    @Test
-    void acceptSuccessNotifiesSenderOfOrder() {
-        Fixture fixture = new Fixture();
-        rider(fixture, 8L);
-        FlashDeliveryOrder waiting = order(10L, "WAITING_ACCEPTANCE");
-        FlashDeliveryOrder accepted = order(10L, ACCEPTED);
-        accepted.setRiderId(8L);
-        when(fixture.orderMapper.selectById(10L)).thenReturn(waiting, accepted);
-        when(fixture.orderMapper.accept(eq(10L), eq(8L), any(Date.class))).thenReturn(1);
-
-        fixture.service.accept(8L, 10L);
-
-        verify(fixture.notificationService).notifyAccepted(waiting);
-    }
-
-    @Test
-    void acceptFailureDoesNotTriggerNotification() {
-        Fixture fixture = new Fixture();
-        rider(fixture, 8L);
-        when(fixture.orderMapper.selectById(10L)).thenReturn(order(10L, "WAITING_ACCEPTANCE"));
-        when(fixture.orderMapper.accept(eq(10L), eq(8L), any(Date.class))).thenReturn(0);
-
-        assertThrows(ServiceException.class, () -> fixture.service.accept(8L, 10L));
-
-        verify(fixture.notificationService, never()).notifyAccepted(any());
-    }
-
-    @Test
-    void pickupSuccessNotifiesSenderOfOrder() {
-        Fixture fixture = new Fixture();
-        rider(fixture, 8L);
-        FlashDeliveryOrder accepted = order(10L, ACCEPTED);
-        accepted.setRiderId(8L);
-        when(fixture.orderMapper.selectById(10L)).thenReturn(accepted);
-        // 逐站取件走 updateFulfilment 条件更新而非 transitionByRider。
-        when(fixture.orderMapper.updateFulfilment(any())).thenReturn(1);
-        FlashDeliveryProofRequest request = new FlashDeliveryProofRequest();
-        request.setImageUrls(List.of("https://example.com/pickup.jpg"));
-
-        fixture.service.pickup(8L, 10L, request);
-
-        verify(fixture.notificationService).notifyPickedUp(accepted);
-    }
-
-    @Test
-    void deliverSuccessNotifiesSenderOfOrder() {
-        Fixture fixture = new Fixture();
-        rider(fixture, 8L);
-        FlashDeliveryOrder pickedUp = order(10L, "PICKED_UP");
-        pickedUp.setRiderId(8L);
-        when(fixture.orderMapper.selectById(10L)).thenReturn(pickedUp);
-        when(fixture.orderMapper.transitionByRider(eq(10L), eq(8L), eq("PICKED_UP"),
-                eq("DELIVERED"), eq("delivered_at"), any())).thenReturn(1);
-        FlashDeliveryDeliverRequest request = new FlashDeliveryDeliverRequest();
-        request.setPinCode("4821");
-        request.setImageUrls(List.of("https://example.com/delivery.jpg"));
-
-        fixture.service.deliver(8L, 10L, request);
-
-        verify(fixture.notificationService).notifyDelivered(pickedUp);
-    }
-
-    @Test
-    void proofTransitionFailureDoesNotTriggerNotification() {
-        Fixture fixture = new Fixture();
-        rider(fixture, 8L);
-        FlashDeliveryOrder accepted = order(10L, ACCEPTED);
-        accepted.setRiderId(8L);
-        when(fixture.orderMapper.selectById(10L)).thenReturn(accepted);
-        when(fixture.orderMapper.transitionByRider(eq(10L), eq(8L), eq("ACCEPTED"),
-                eq("PICKED_UP"), eq("picked_up_at"), any())).thenReturn(0);
-        FlashDeliveryProofRequest request = new FlashDeliveryProofRequest();
-        request.setImageUrls(List.of("https://example.com/pickup.jpg"));
-
-        assertThrows(ServiceException.class, () -> fixture.service.pickup(8L, 10L, request));
-
-        verify(fixture.notificationService, never()).notifyPickedUp(any());
-        verify(fixture.notificationService, never()).notifyDelivered(any());
-    }
-
-    @Test
-    void userCancelSuccessNotifiesAssignedRider() {
-        Fixture fixture = new Fixture();
-        FlashDeliveryOrder accepted = order(10L, ACCEPTED);
-        accepted.setRiderId(9L);
-        when(fixture.orderMapper.selectById(10L)).thenReturn(accepted);
-        when(fixture.orderMapper.cancel(eq(10L), eq("ACCEPTED"), eq("USER"), eq(7L),
-                anyString(), any(Date.class))).thenReturn(1);
-
-        fixture.service.userCancel(7L, 10L, reason("改行程"));
-
-        verify(fixture.notificationService).notifyCancelled(accepted);
-    }
-
-    @Test
-    void adminCancelSuccessNotifiesAssignedRider() {
-        Fixture fixture = new Fixture();
-        FlashDeliveryOrder pickedUp = order(10L, "PICKED_UP");
-        pickedUp.setRiderId(9L);
-        when(fixture.orderMapper.selectById(10L)).thenReturn(pickedUp);
-        // 平台异常取消先写介入审计,再走条件取消。
-        when(fixture.orderMapper.update(any(), any())).thenReturn(1);
-        when(fixture.orderMapper.cancel(eq(10L), eq("PICKED_UP"), eq("ADMIN"), eq(1L),
-                anyString(), any(Date.class))).thenReturn(1);
-        FlashDeliveryReasonRequest request = reason("异常收口");
-        request.setProblemStopId(101L);
-        request.setHandlingResult("平台介入收口");
-        // 已取货订单必须记录物品去向和交接凭证。
-        request.setPickedGoodsDisposition("已取物品交回寄件人");
-        request.setHandoffImageUrls(List.of("https://example.com/handoff.jpg"));
-
-        fixture.service.adminCancel(1L, 10L, request);
-
-        verify(fixture.notificationService).notifyCancelled(pickedUp);
-    }
-
-    @Test
-    void cancelFailureDoesNotTriggerNotification() {
-        Fixture fixture = new Fixture();
-        FlashDeliveryOrder accepted = order(10L, ACCEPTED);
-        accepted.setRiderId(9L);
-        when(fixture.orderMapper.selectById(10L)).thenReturn(accepted);
-        when(fixture.orderMapper.cancel(eq(10L), eq("ACCEPTED"), eq("USER"), eq(7L),
-                anyString(), any(Date.class))).thenReturn(0);
-
-        assertThrows(ServiceException.class, () -> fixture.service.userCancel(7L, 10L, reason("改行程")));
-
-        verify(fixture.notificationService, never()).notifyCancelled(any());
-    }
-
-    @Test
-    void excludedEventsNeverTriggerNotification() {
-        // 加小费、完成、过期预约自动取消三类事件不产生任何推送。
-        Fixture fixture = new Fixture();
-        editableOrder(fixture);
-        when(fixture.orderMapper.updateWaitingTip(any(), eq(2))).thenReturn(1);
-        fixture.service.addTip(7L, 10L, tipRequest(20L, 2));
-
-        // 合并后的 complete 要求订单已送达且全部取货站完成。
-        when(fixture.orderMapper.selectById(10L)).thenReturn(order(10L, "DELIVERED"));
-        when(fixture.orderMapper.transitionByStatus(eq(10L), eq("DELIVERED"), eq("COMPLETED"),
-                eq("completed_at"), any(Date.class))).thenReturn(1);
-        assertTrue(fixture.service.complete(1L, "ADMIN", 10L));
-
-        when(fixture.orderMapper.cancelExpiredScheduled(eq(10L), any(Date.class))).thenReturn(1);
-        assertTrue(fixture.service.cancelExpiredScheduledOrder(10L, new Date()));
-
-        verifyNoInteractions(fixture.notificationService);
-    }
-
-    @Test
-    void immediateOrderIsVisibleAndNotifiesAvailableRiders() {
-        Fixture fixture = new Fixture();
-        prepareCreate(fixture);
-        FlashDeliveryCreateRequest request = createRequest();
-        request.setDeliveryMode("NOW");
-        request.setScheduledPickupStartAt(null);
-        request.setScheduledPickupEndAt(null);
-
-        fixture.service.create(7L, request);
-
-        ArgumentCaptor<FlashDeliveryOrder> saved = ArgumentCaptor.forClass(FlashDeliveryOrder.class);
-        verify(fixture.orderMapper).insert(saved.capture());
-        assertTrue(saved.getValue().getIsDisplay());
-        verify(fixture.notificationService).notifyAvailable(saved.getValue());
-    }
-
-    @Test
-    void scheduledOrderStartsHiddenAndDoesNotNotifyBeforeWindow() {
-        Fixture fixture = new Fixture();
-        prepareCreate(fixture);
-
-        fixture.service.create(7L, createRequest());
-
-        ArgumentCaptor<FlashDeliveryOrder> saved = ArgumentCaptor.forClass(FlashDeliveryOrder.class);
-        verify(fixture.orderMapper).insert(saved.capture());
-        assertFalse(saved.getValue().getIsDisplay());
-        verify(fixture.notificationService, never()).notifyAvailable(any());
-    }
-
-    @Test
-    void scheduledOrderBecomesVisibleAndNotifiesOnlyAfterAtomicOpenSucceeds() {
-        Fixture fixture = new Fixture();
-        FlashDeliveryOrder scheduled = order(10L, "WAITING_ACCEPTANCE");
-        scheduled.setDeliveryMode("SCHEDULED");
-        scheduled.setIsDisplay(false);
-        Date now = new Date();
-        when(fixture.orderMapper.selectById(10L)).thenReturn(scheduled);
-        when(fixture.orderMapper.openScheduledForRiders(10L, now)).thenReturn(1);
-
-        assertTrue(fixture.service.openScheduledOrderForRiders(10L, now));
-
-        assertTrue(scheduled.getIsDisplay());
-        verify(fixture.notificationService).notifyAvailable(scheduled);
-
-        when(fixture.orderMapper.openScheduledForRiders(10L, now)).thenReturn(0);
-        assertFalse(fixture.service.openScheduledOrderForRiders(10L, now));
-        verify(fixture.notificationService).notifyAvailable(scheduled);
-    }
-
-    private FlashDeliveryReasonRequest reason(String text) {
-        FlashDeliveryReasonRequest request = new FlashDeliveryReasonRequest();
-        request.setReason(text);
-        return request;
-    }
-
-    @Test
-    void oversizedProofUrlIsRejectedBeforeStateTransition() {
-        Fixture fixture = new Fixture();
-        InfoUser rider = new InfoUser();
-        rider.setUserId(8L);
-        rider.setUserType("2");
-        when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
-        FlashDeliveryProofRequest request = new FlashDeliveryProofRequest();
-        request.setImageUrls(List.of("https://example.com/" + "a".repeat(1000)));
-
-        assertThrows(RuntimeException.class, () -> fixture.service.pickup(8L, 10L, request));
-        verify(fixture.orderMapper, never()).transitionByRider(any(), any(), any(), any(), any(), any());
-    }
-
-    @Test
-    void missingServiceTypeReturnsBusinessErrorInsteadOfNullPointer() {
-        Fixture fixture = new Fixture();
-        FlashDeliveryQuoteRequest request = request();
-        request.setServiceType(null);
-        assertThrows(ServiceException.class, () -> fixture.service.quote(request));
-    }
-
-    @Test
-    void equalTextAddressesAreRejectedEvenWhenCoordinatesDiffer() {
-        Fixture fixture = new Fixture();
-        FlashDeliveryQuoteRequest request = request();
-        request.getDelivery().setAddress(request.getPickup().getAddress());
-        request.getDelivery().setAddressDetail(request.getPickup().getAddressDetail());
-        assertThrows(ServiceException.class, () -> fixture.service.quote(request));
-    }
-
-    @Test
-    void concurrentPricingUpdateIsRejected() {
-        Fixture fixture = new Fixture();
-        when(fixture.pricingMapper.selectById(1L)).thenReturn(pricing());
-        when(fixture.pricingMapper.countOverlapping(eq("00:00"), eq("12:00"), anyInt(), eq(1L))).thenReturn(0);
-        when(fixture.pricingMapper.update(isNull(), any(Wrapper.class))).thenReturn(0);
-        FlashDeliveryPricingRequest request = pricingRequest();
-        request.setEndTime("12:00");
-        request.setConfigVersion(1);
-
-        assertThrows(ServiceException.class,
-                () -> fixture.service.updatePricing(1L, 1L, request));
-        verify(fixture.pricingMapper, never()).updateById(any(FlashDeliveryPricing.class));
-    }
-
-    @Test
-    void overlappingPricingPeriodIsRejectedBeforeInsert() {
-        Fixture fixture = new Fixture();
-        FlashDeliveryPricingRequest request = pricingRequest();
-        when(fixture.pricingMapper.countOverlapping(eq("00:00"), eq("24:00"), anyInt(), isNull())).thenReturn(1);
-
-        assertThrows(ServiceException.class, () -> fixture.service.createPricing(1L, request));
-
-        request.setDistance(new BigDecimal("0.004"));
-        assertThrows(ServiceException.class, () -> fixture.service.createPricing(1L, request));
-
-        verify(fixture.pricingMapper, never()).insert(any(FlashDeliveryPricing.class));
-    }
-
-    @Test
-    void pricingDistanceHalfCentimetreNormalizesToDatabaseMinimum() {
-        Fixture fixture = new Fixture();
-        FlashDeliveryPricingRequest request = pricingRequest();
-        request.setDistance(new BigDecimal("0.005"));
-
-        fixture.service.createPricing(1L, request);
-
-        ArgumentCaptor<FlashDeliveryPricing> saved = ArgumentCaptor.forClass(FlashDeliveryPricing.class);
-        verify(fixture.pricingMapper).insert(saved.capture());
-        assertEquals(new BigDecimal("0.01"), saved.getValue().getDistance());
-    }
-
-    @Test
-    void pricingDistanceThatRoundsToZeroIsRejectedBeforePersistence() {
-        Fixture fixture = new Fixture();
-        FlashDeliveryPricingRequest request = pricingRequest();
-        request.setDistance(new BigDecimal("0.001"));
-
-        assertThrows(ServiceException.class, () -> fixture.service.createPricing(1L, request));
-
-        verify(fixture.pricingMapper, never()).insert(any(FlashDeliveryPricing.class));
-    }
-
-    @Test
-    void createPricingPersistsImmediatelyActiveTimePeriod() {
-        Fixture fixture = new Fixture();
-        FlashDeliveryPricingRequest request = pricingRequest();
-        when(fixture.pricingMapper.countOverlapping(eq("00:00"), eq("24:00"), anyInt(), isNull())).thenReturn(0);
-
-        fixture.service.createPricing(7L, request);
-
-        ArgumentCaptor<FlashDeliveryPricing> saved = ArgumentCaptor.forClass(FlashDeliveryPricing.class);
-        verify(fixture.pricingMapper).insert(saved.capture());
-        assertEquals(1, saved.getValue().getConfigVersion());
-        assertEquals(7L, saved.getValue().getUpdatedBy());
-        assertEquals(90L, saved.getValue().getStartingFare());
-
-        InOrder writeOrder = inOrder(fixture.pricingMapper);
-        writeOrder.verify(fixture.pricingMapper)
-                .countOverlapping(eq("00:00"), eq("24:00"), anyInt(), isNull());
-        writeOrder.verify(fixture.pricingMapper).insert(any(FlashDeliveryPricing.class));
-    }
-
-    @Test
-    void homeOmitsServiceWithoutCurrentPricingPeriod() {
-        Fixture fixture = new Fixture();
-
-        assertTrue(fixture.service.home().getServices().isEmpty());
-    }
-
-    @Test
-    void quoteWithoutCurrentPricingPeriodFailsBeforeRouteLookup() {
-        Fixture fixture = new Fixture();
-
-        assertThrows(ServiceException.class, () -> fixture.service.quote(request()));
-
-        verify(fixture.routeService, never()).calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt());
-    }
-
-    @Test
-    void multipleCurrentPricingPeriodsFailExplicitlyBeforeRouteLookup() {
-        Fixture fixture = new Fixture();
-        when(fixture.pricingMapper.selectAtTime(anyString(), anyInt()))
-                .thenReturn(List.of(pricing(), pricing()));
-
-        assertThrows(ServiceException.class, () -> fixture.service.quote(request()));
-
-        verify(fixture.routeService, never()).calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt());
-    }
-
-    @Test
-    void updatingPricingChecksOverlapBeforeVersionedWrite() {
-        Fixture fixture = new Fixture();
-        FlashDeliveryPricing existing = pricing();
-        when(fixture.pricingMapper.selectById(1L)).thenReturn(existing);
-        when(fixture.pricingMapper.update(isNull(), any(Wrapper.class))).thenReturn(1);
-        FlashDeliveryPricingRequest request = pricingRequest();
-        request.setConfigVersion(1);
-
-        fixture.service.updatePricing(9L, 1L, request);
-
-        InOrder writeOrder = inOrder(fixture.pricingMapper);
-        writeOrder.verify(fixture.pricingMapper)
-                .countOverlapping(eq("00:00"), eq("24:00"), anyInt(), eq(1L));
-        writeOrder.verify(fixture.pricingMapper).update(isNull(), any(Wrapper.class));
-    }
-
-    @Test
-    void deletePricingRemovesConfigurationImmediately() {
-        Fixture fixture = new Fixture();
-        when(fixture.pricingMapper.selectById(18L)).thenReturn(pricing());
-        when(fixture.pricingMapper.deleteById(18L)).thenReturn(1);
-
-        fixture.service.deletePricing(18L);
-
-        verify(fixture.pricingMapper).deleteById(18L);
-    }
-
-    @Test
-    void urgentQuoteReturnsSeparatedFeesAndUsesScheduledPickupTime() {
-        Fixture fixture = new Fixture();
-        when(fixture.pricingMapper.selectAtTime(eq("16:30"), anyInt())).thenReturn(List.of(pricing()));
-        when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).thenReturn(new RouteDistance(6000, 900, "ROUTE"));
-        FlashDeliveryQuoteRequest request = request();
-        request.setDeliveryType("URGENT");
-        request.setTipAmount(5L);
-        request.setDeliveryMode("SCHEDULED");
-        Calendar calendar = Calendar.getInstance();
-        calendar.add(Calendar.DAY_OF_MONTH, 1);
-        calendar.set(Calendar.HOUR_OF_DAY, 16);
-        calendar.set(Calendar.MINUTE, 30);
-        calendar.set(Calendar.SECOND, 0);
-        calendar.set(Calendar.MILLISECOND, 0);
-        request.setScheduledPickupStartAt(calendar.getTime());
-        request.setScheduledPickupEndAt(new Date(calendar.getTimeInMillis() + 30L * 60L * 1000L));
-
-        var quote = fixture.service.quote(request);
-
-        assertEquals(45L, quote.getDistanceFee());
-        // 起步价与 45 元距离附加费分开返回,避免前端重复相加。
-        assertEquals(90L, quote.getBaseDeliveryFee());
-        assertEquals(27L, quote.getUrgentFee());
-        assertEquals(5L, quote.getTipAmount());
-        assertEquals(167L, quote.getAmount());
-        verify(fixture.pricingMapper).selectAtTime(eq("16:30"), anyInt());
-    }
-
-    @Test
-    void createRejectsChangedQuoteAndReturnsLatestQuoteWithoutInsert() {
-        Fixture fixture = new Fixture();
-        when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
-        when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).thenReturn(new RouteDistance(3200, 600, "ROUTE"));
-        FlashDeliveryCreateRequest request = createRequest();
-        request.setQuotedAmount(91L);
-
-        FlashDeliveryQuoteChangedException error = assertThrows(FlashDeliveryQuoteChangedException.class,
-                () -> fixture.service.create(7L, request));
-
-        assertEquals(90L, error.getLatestQuote().getAmount());
-        assertEquals(1L, error.getLatestQuote().getPricingId());
-        verify(fixture.orderMapper, never()).insert(any(FlashDeliveryOrder.class));
-    }
-
-    @Test
-    void createDefaultsPayTypeToCashAndStoresTransferSelection() {
-        Fixture fixture = new Fixture();
-        when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
-        when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).thenReturn(new RouteDistance(3200, 600, "ROUTE"));
-        doAnswer(invocation -> {
-            FlashDeliveryOrder order = invocation.getArgument(0);
-            order.setId(99L);
-            return 1;
-        }).when(fixture.orderMapper).insert(any(FlashDeliveryOrder.class));
-        when(fixture.imageMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
-        when(fixture.logMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
-
-        var defaultResult = fixture.service.create(7L, createRequest());
-        FlashDeliveryCreateRequest transfer = createRequest();
-        transfer.setPayType("6");
-        var transferResult = fixture.service.create(7L, transfer);
-
-        ArgumentCaptor<FlashDeliveryOrder> saved = ArgumentCaptor.forClass(FlashDeliveryOrder.class);
-        verify(fixture.orderMapper, times(2)).insert(saved.capture());
-        assertEquals("4", saved.getAllValues().get(0).getPayType());
-        assertEquals("6", saved.getAllValues().get(1).getPayType());
-        assertEquals("4", defaultResult.getPayType());
-        assertEquals("6", transferResult.getPayType());
-    }
-
-    @Test
-    void createRejectsUnsupportedPayType() {
-        Fixture fixture = new Fixture();
-        when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
-        when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).thenReturn(new RouteDistance(3200, 600, "ROUTE"));
-        FlashDeliveryCreateRequest request = createRequest();
-        request.setPayType("2");
-        org.mockito.Mockito.doThrow(new ServiceException("flash.delivery.pay.type.invalid"))
-                .when(fixture.paymentMethodGateService).assertUsable(eq("2"),
-                        eq(com.ruoyi.system.service.PaymentMethodGateService.GateScope.RIDER_FLASH), isNull(), isNull());
-
-        assertThrows(ServiceException.class, () -> fixture.service.create(7L, request));
-
-        verify(fixture.orderMapper, never()).insert(any(FlashDeliveryOrder.class));
-    }
-
-    @Test
-    void quoteValidatesDeliveryLevelAndCompleteItemInformation() {
-        Fixture fixture = new Fixture();
-        FlashDeliveryQuoteRequest request = request();
-        request.setDeliveryType("FAST");
-        FlashDeliveryQuoteRequest invalidDeliveryType = request;
-        assertThrows(ServiceException.class, () -> fixture.service.quote(invalidDeliveryType));
-
-        request = request();
-        request.setQuantity(0);
-        FlashDeliveryQuoteRequest invalidQuantity = request;
-        assertThrows(ServiceException.class, () -> fixture.service.quote(invalidQuantity));
-
-        request = request();
-        request.setWeightRange("SMALL");
-        FlashDeliveryQuoteRequest oldWeightRange = request;
-        assertThrows(ServiceException.class, () -> fixture.service.quote(oldWeightRange));
-    }
-
-    @Test
-    void quoteAcceptsAllFourWeightRanges() {
-        Fixture fixture = new Fixture();
-        when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
-        when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).thenReturn(new RouteDistance(3200, 600, "ROUTE"));
-
-        for (String weightRange : List.of("UP_TO_5_KG", "OVER_5_TO_10_KG",
-                "OVER_10_TO_15_KG", "OVER_15_TO_20_KG")) {
-            FlashDeliveryQuoteRequest request = request();
-            request.setWeightRange(weightRange);
-            var quote = assertDoesNotThrow(() -> fixture.service.quote(request), weightRange);
-            assertEquals(90L, quote.getAmount(), weightRange);
-        }
-    }
-
-    @Test
-    void createPersistsAndReturnsEveryWeightRange() {
-        for (String weightRange : List.of("UP_TO_5_KG", "OVER_5_TO_10_KG",
-                "OVER_10_TO_15_KG", "OVER_15_TO_20_KG")) {
-            Fixture fixture = new Fixture();
-            when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
-            when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).thenReturn(new RouteDistance(3200, 600, "ROUTE"));
-            doAnswer(invocation -> {
-                FlashDeliveryOrder order = invocation.getArgument(0);
-                order.setId(99L);
-                return 1;
-            }).when(fixture.orderMapper).insert(any(FlashDeliveryOrder.class));
-            when(fixture.imageMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
-            when(fixture.logMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
-            FlashDeliveryCreateRequest request = createRequest();
-            request.setWeightRange(weightRange);
-
-            var result = fixture.service.create(7L, request);
-
-            ArgumentCaptor<FlashDeliveryOrder> saved = ArgumentCaptor.forClass(FlashDeliveryOrder.class);
-            verify(fixture.orderMapper).insert(saved.capture());
-            assertEquals(weightRange, saved.getValue().getWeightRange());
-            assertEquals(weightRange, result.getWeightRange());
-        }
-    }
-
-    @Test
-    void quoteAllowsMissingWeightRange() {
-        Fixture fixture = new Fixture();
-        when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
-        when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt()))
-                .thenReturn(new RouteDistance(3200, 600, "ROUTE"));
-
-        FlashDeliveryQuoteRequest request = request();
-        request.setWeightRange(null);
-
-        assertDoesNotThrow(() -> fixture.service.quote(request));
-    }
-
-    @Test
-    void quoteRejectsUnknownWeightRangeWhenProvided() {
-        Fixture fixture = new Fixture();
-        for (String weightRange : new String[]{"", "SMALL", "OVER_20_KG"}) {
-            FlashDeliveryQuoteRequest request = request();
-            request.setWeightRange(weightRange);
-            assertThrows(ServiceException.class, () -> fixture.service.quote(request), String.valueOf(weightRange));
-        }
-    }
-
-    private FlashDeliveryQuoteRequest request() {
-        FlashDeliveryQuoteRequest request = new FlashDeliveryQuoteRequest();
-        request.setServiceType("HELP_SEND");
-        request.setDeliveryType("NORMAL");
-        request.setVehicleType(1);
-        request.setPackageType("DOCUMENT");
-        request.setQuantity(1);
-        request.setWeightRange("UP_TO_5_KG");
-        request.setSpecification("30 x 20 x 10 cm");
-        request.setTipAmount(0L);
-        request.setDeliveryMode("NOW");
-        request.setPickup(address("A", "0911111111", "25.0330", "121.5645"));
-        request.setDelivery(address("B", "0922222222", "25.0478", "121.5319"));
-        request.getDelivery().setAddress("second road");
-        return request;
-    }
-
-    private FlashDeliveryCreateRequest createRequest() {
-        FlashDeliveryCreateRequest request = new FlashDeliveryCreateRequest();
-        FlashDeliveryQuoteRequest quote = request();
-        request.setServiceType(quote.getServiceType());
-        request.setDeliveryType(quote.getDeliveryType());
-        request.setVehicleType(quote.getVehicleType());
-        request.setPackageType(quote.getPackageType());
-        request.setQuantity(quote.getQuantity());
-        request.setWeightRange(quote.getWeightRange());
-        request.setSpecification(quote.getSpecification());
-        request.setTipAmount(quote.getTipAmount());
-        request.setPickup(quote.getPickup());
-        request.setDelivery(quote.getDelivery());
-        request.setClientRequestId("flash-scheduled-001");
-        request.setDeliveryMode("SCHEDULED");
-        Date start = new Date(System.currentTimeMillis() + 60L * 60L * 1000L);
-        request.setScheduledPickupStartAt(start);
-        request.setScheduledPickupEndAt(new Date(start.getTime() + 30L * 60L * 1000L));
-        request.setPinRequired(true);
-        request.setPricingId(1L);
-        request.setPricingVersion(1);
-        request.setQuotedBaseDeliveryFee(90L);
-        request.setQuotedDistanceFee(0L);
-        request.setQuotedUrgentFee(0L);
-        request.setQuotedAmount(90L);
-        request.setSenderImageUrls(List.of("https://example.com/sender.jpg"));
-        return request;
-    }
-
-    private void prepareCreate(Fixture fixture) {
-        when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
-        when(fixture.routeService.calculate(any(), any(), anyInt()))
-                .thenReturn(new RouteDistance(3200, 600, "ROUTE"));
-        when(fixture.userMapper.selectOrdinaryUserIdsByNormalizedPhone("0922222222")).thenReturn(List.of());
-        doAnswer(invocation -> {
-            FlashDeliveryOrder order = invocation.getArgument(0);
-            order.setId(99L);
-            return 1;
-        }).when(fixture.orderMapper).insert(any(FlashDeliveryOrder.class));
-    }
-
-    private FlashDeliveryAddressRequest address(String name, String phone, String lat, String lon) {
-        FlashDeliveryAddressRequest address = new FlashDeliveryAddressRequest();
-        address.setName(name);
-        address.setPhone(phone);
-        address.setAddress("road");
-        address.setAddressDetail("door");
-        address.setLatitude(new BigDecimal(lat));
-        address.setLongitude(new BigDecimal(lon));
-        return address;
-    }
-
-    private FlashDeliveryPricing pricing() {
-        FlashDeliveryPricing pricing = new FlashDeliveryPricing();
-        pricing.setId(1L);
-        pricing.setStartTime("00:00");
-        pricing.setEndTime("24:00");
-        pricing.setStartingDistance(new BigDecimal("3.00"));
-        pricing.setStartingFare(90L);
-        pricing.setDistance(new BigDecimal("1.00"));
-        pricing.setFreight(15L);
-        pricing.setUrgentRate(new BigDecimal("20.00"));
-        pricing.setMinimumUrgentFee(10L);
-        pricing.setConfigVersion(1);
-        return pricing;
-    }
-
-    private FlashDeliveryPricingRequest pricingRequest() {
-        FlashDeliveryPricingRequest request = new FlashDeliveryPricingRequest();
-        request.setVehicleType(1);
-        request.setStartTime("00:00");
-        request.setEndTime("24:00");
-        request.setStartingDistance(new BigDecimal("3.00"));
-        request.setStartingFare(90L);
-        request.setDistance(new BigDecimal("1.00"));
-        request.setFreight(15L);
-        request.setUrgentRate(new BigDecimal("20.00"));
-        request.setMinimumUrgentFee(10L);
-        return request;
-    }
-
-    @SuppressWarnings({"rawtypes", "unchecked"})
-    private void assertRiderTab(String tab, List<String> statuses) {
-        Fixture fixture = new Fixture();
-        rider(fixture, 8L);
-        when(fixture.orderMapper.selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
-                any(Wrapper.class))).thenReturn(new Page<FlashDeliveryOrder>());
-
-        fixture.service.riderOrders(8L, 1, 10, tab, null, null);
-
-        ArgumentCaptor<com.baomidou.mybatisplus.core.conditions.query.QueryWrapper> query =
-                ArgumentCaptor.forClass(com.baomidou.mybatisplus.core.conditions.query.QueryWrapper.class);
-        verify(fixture.orderMapper).selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class), query.capture());
-        // MyBatis-Plus 的条件参数在渲染 SQL 段时才写入参数表,必须先取 SQL 再断言参数。
-        String sql = query.getValue().getSqlSegment();
-        assertTrue(sql.contains("rider_id"), tab);
-        var parameters = query.getValue().getParamNameValuePairs().values();
-        assertTrue(parameters.contains(8L), tab);
-        for (String status : statuses) assertTrue(parameters.contains(status), tab + ":" + status);
-    }
-
-    private void rider(Fixture fixture, Long riderId) {
-        InfoUser rider = new InfoUser();
-        rider.setUserId(riderId);
-        rider.setUserType("2");
-        rider.setDeliveryType("FLASH");
-        when(fixture.userMapper.selectInfoUserByUserId(riderId)).thenReturn(rider);
-    }
-
-    private com.ruoyi.app.flashdelivery.dto.FlashDeliveryRatingRequest rating(Double stars) {
-        com.ruoyi.app.flashdelivery.dto.FlashDeliveryRatingRequest request =
-                new com.ruoyi.app.flashdelivery.dto.FlashDeliveryRatingRequest();
-        request.setStars(stars);
-        return request;
-    }
-
-    private FlashDeliveryOrder order(Long id, String status) {
-        FlashDeliveryOrder order = new FlashDeliveryOrder();
-        order.setId(id);
-        order.setPickupStopCount(1); order.setPickedUpStopCount(0); order.setVersion(0);
-        order.setOrderNo("FL-20260813-021");
-        order.setUserId(7L);
-        order.setServiceType("HELP_SEND");
-        order.setDeliveryType("NORMAL");
-        order.setStatus(status);
-        order.setIsDisplay(true);
-        order.setPackageType("DOCUMENT");
-        order.setQuantity(1);
-        order.setWeightRange("UP_TO_5_KG");
-        order.setDeliveryMode("NOW");
-        order.setPinRequired(true);
-        order.setDeliveryPinCode("4821");
-        order.setPickupName("pickup contact");
-        order.setPickupPhone("0911111111");
-        order.setPickupAddress("pickup road");
-        order.setPickupAddressDetail("pickup door");
-        order.setPickupCity("台北市");
-        order.setPickupArea("中山區");
-        order.setPickupLongitude(new BigDecimal("121.5100"));
-        order.setPickupLatitude(new BigDecimal("25.0100"));
-        order.setDeliveryName("delivery contact");
-        order.setDeliveryPhone("0922222222");
-        order.setDeliveryAddress("delivery road");
-        order.setDeliveryAddressDetail("delivery door");
-        order.setDeliveryCity("台北市");
-        order.setDeliveryArea("大安區");
-        order.setDeliveryLongitude(new BigDecimal("121.5200"));
-        order.setDeliveryLatitude(new BigDecimal("25.0200"));
-        order.setDistanceMeters(3200);
-        order.setEstimatedDurationSeconds(2100);
-        order.setDistanceFee(12L);
-        order.setBaseDeliveryFee(102L);
-        order.setUrgentRate(new BigDecimal("20.00"));
-        order.setMinimumUrgentFee(10L);
-        order.setUrgentFee(0L);
-        order.setTipAmount(10L);
-        order.setAmount(112L);
-        order.setCurrency("TWD");
-        order.setUserNote("private note");
-        order.setCreateTime(new Date());
-        return order;
-    }
-
-    private FlashDeliveryOrderImage image(String proofType, String url) {
-        FlashDeliveryOrderImage image = new FlashDeliveryOrderImage();
-        image.setProofType(proofType);
-        image.setImageUrl(url);
-        return image;
-    }
-
-
-    @Test
-    void missingOrInvalidBodyOrderIdIsRejectedBeforeDatabaseAccess() {
-        Fixture fixture = new Fixture();
-        for (Long orderId : java.util.Arrays.asList(null, 0L, -1L)) {
-            ServiceException addressFailure = assertThrows(ServiceException.class,
-                    () -> fixture.service.updateAddress(7L, orderId, new FlashDeliveryAddressConfirmRequest()));
-            ServiceException tipFailure = assertThrows(ServiceException.class,
-                    () -> fixture.service.addTip(7L, orderId, tipRequest(20L, 2)));
-            assertEquals("flash.delivery.order.not.found", addressFailure.getMessage());
-            assertEquals("flash.delivery.order.not.found", tipFailure.getMessage());
-        }
-        org.mockito.Mockito.verifyNoInteractions(fixture.orderMapper);
-    }
-
-    @Test
-    void addressQuoteUsesOriginalPricingSnapshotAndDoesNotWrite() {
-        Fixture fixture = new Fixture();
-        FlashDeliveryOrder order = editableOrder(fixture);
-        // FR-074/093:预约单也保留原运价快照,平台调价不得改变旧单的计价参数。
-        order.setDeliveryMode("SCHEDULED");
-        order.setScheduledPickupStartAt(new Date(1));
-        order.setScheduledPickupEndAt(new Date(1800001));
-        FlashDeliveryQuoteView quote = fixture.service.quoteAddress(7L, 10L, addressChange());
-        assertEquals(4500, quote.getDistanceMeters());
-        assertEquals(900, quote.getEstimatedDurationSeconds());
-        assertEquals(23L, quote.getDistanceFee());
-        // 基础配送费只表示起步价,距离附加费单独返回。
-        assertEquals(90L, quote.getBaseDeliveryFee());
-        assertEquals(10L, quote.getTipAmount());
-        assertEquals(123L, quote.getAmount());
-        assertEquals(2, quote.getOrderVersion());
-        assertEquals(1, quote.getPricingVersion());
-        assertEquals("pickup road", order.getPickupAddress());
-        // 地址变化只重算路线,不查询最新运价。
-        verify(fixture.pricingMapper, never()).selectAtTime(anyString(), anyInt());
-        verify(fixture.orderMapper, never()).updateWaitingAddress(any(), any());
-        verify(fixture.logMapper, never()).insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderLog.class));
-    }
-
-    @Test
-    void addressSaveUpdatesBothEndsRouteFeesAndReceiverTogether() {
-        Fixture fixture = new Fixture();
-        FlashDeliveryOrder order = editableOrder(fixture);
-        when(fixture.userMapper.selectOrdinaryUserIdsByNormalizedPhone("0922222222"))
-                .thenReturn(List.of(88L));
-        when(fixture.orderMapper.updateWaitingAddress(any(), eq(2))).thenReturn(1);
-        FlashDeliveryAddressConfirmRequest input = confirmAddress(fixture);
-        input.getPickup().setName(" new sender ");
-        input.getPickup().setAddressDetail(null);
-        input.getDelivery().setName("new receiver");
-        var view = fixture.service.updateAddress(7L, 10L, input);
-        assertEquals("new sender", view.getPickup().getName());
-        assertEquals("new receiver", view.getDelivery().getName());
-        assertNull(view.getPickup().getAddressDetail());
-        assertEquals(4500, view.getDistanceMeters());
-        assertEquals(123L, view.getAmount());
-        assertEquals(3, view.getOrderVersion());
-        assertEquals(88L, order.getReceiverUserId());
-        assertEquals(10L, order.getTipAmount());
-        assertEquals("WAITING_ACCEPTANCE", order.getStatus());
-        verify(fixture.orderMapper).updateWaitingAddress(order, 2);
-        verify(fixture.logMapper).insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderLog.class));
-    }
-
-    @Test
-    void addressChangeClearsFormerReceiverWhenNewPhoneDoesNotMatch() {
-        Fixture fixture = new Fixture();
-        FlashDeliveryOrder order = editableOrder(fixture);
-        order.setReceiverUserId(88L);
-        when(fixture.orderMapper.updateWaitingAddress(any(), eq(2))).thenReturn(1);
-        fixture.service.updateAddress(7L, 10L, confirmAddress(fixture));
-        assertNull(order.getReceiverUserId());
-    }
-
-    @Test
-    void addressQuotePreservesUrgentRulesAndExistingTip() {
-        Fixture fixture = new Fixture();
-        FlashDeliveryOrder order = editableOrder(fixture);
-        order.setDeliveryType("URGENT");
-        var quote = fixture.service.quoteAddress(7L, 10L, addressChange());
-        assertEquals(23L, quote.getUrgentFee());
-        assertEquals(146L, quote.getAmount());
-        assertEquals(10L, quote.getTipAmount());
-    }
-
-    @Test
-    void changedRouteOrUnconfirmedPriceReturnsLatestQuoteWithoutSaving() {
-        Fixture fixture = new Fixture();
-        editableOrder(fixture);
-        FlashDeliveryAddressConfirmRequest input = confirmAddress(fixture);
-        input.setQuotedAmount(1L);
-        var exception = assertThrows(FlashDeliveryQuoteChangedException.class,
-                () -> fixture.service.updateAddress(7L, 10L, input));
-        assertEquals(123L, exception.getLatestQuote().getAmount());
-        input.setQuotedAmount(123L);
-        input.setQuotedDistanceMeters(4499);
-        assertThrows(FlashDeliveryQuoteChangedException.class,
-                () -> fixture.service.updateAddress(7L, 10L, input));
-        input.setQuotedDistanceMeters(4500);
-        input.setQuotedUrgentFee(null);
-        assertThrows(FlashDeliveryQuoteChangedException.class,
-                () -> fixture.service.updateAddress(7L, 10L, input));
-        verify(fixture.orderMapper, never()).updateWaitingAddress(any(), any());
-        verify(fixture.logMapper, never()).insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderLog.class));
-    }
-
-    @Test
-    void receiverAndOtherUsersCannotQuoteEditOrAddTip() {
-        Fixture fixture = new Fixture();
-        FlashDeliveryOrder order = editableOrder(fixture);
-        order.setReceiverUserId(88L);
-        for (Long user : List.of(88L, 99L)) {
-            assertThrows(ServiceException.class, () -> fixture.service.quoteAddress(user, 10L, addressChange()));
-            assertThrows(ServiceException.class, () -> fixture.service.updateAddress(user, 10L, new FlashDeliveryAddressConfirmRequest()));
-            assertThrows(ServiceException.class, () -> fixture.service.addTip(user, 10L, tipRequest(5L, 2)));
-        }
-        verify(fixture.routeService, never()).calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt());
-        verify(fixture.orderMapper, never()).updateWaitingTip(any(), any());
-    }
-
-    @Test
-    void everyNonWaitingStateAndAssignedRiderRejectsBothChanges() {
-        for (String status : List.of("ACCEPTED", "PICKED_UP", "DELIVERED", "COMPLETED", "CANCELLED")) {
-            Fixture fixture = new Fixture();
-            FlashDeliveryOrder order = editableOrder(fixture);
-            order.setStatus(status);
-            assertThrows(ServiceException.class, () -> fixture.service.quoteAddress(7L, 10L, addressChange()));
-            assertThrows(ServiceException.class, () -> fixture.service.updateAddress(7L, 10L, new FlashDeliveryAddressConfirmRequest()));
-            assertThrows(ServiceException.class, () -> fixture.service.addTip(7L, 10L, tipRequest(5L, 2)));
-        }
-        Fixture fixture = new Fixture();
-        editableOrder(fixture).setRiderId(8L);
-        assertThrows(ServiceException.class, () -> fixture.service.quoteAddress(7L, 10L, addressChange()));
-        assertThrows(ServiceException.class, () -> fixture.service.addTip(7L, 10L, tipRequest(5L, 2)));
-    }
-
-    @Test
-    void staleOrMissingVersionCannotOverwriteNewerOrder() {
-        Fixture fixture = new Fixture();
-        editableOrder(fixture);
-        FlashDeliveryAddressChangeRequest input = addressChange();
-        input.setOrderVersion(1);
-        assertThrows(ServiceException.class, () -> fixture.service.quoteAddress(7L, 10L, input));
-        for (Integer version : java.util.Arrays.asList(null, -1, 1)) {
-            assertThrows(ServiceException.class,
-                    () -> fixture.service.addTip(7L, 10L, tipRequest(5L, version)));
-        }
-        verify(fixture.orderMapper, never()).updateWaitingTip(any(), any());
-    }
-
-    @Test
-    void addressValidationRejectsSameInvalidAndOverDistanceEndpoints() {
-        Fixture fixture = new Fixture();
-        editableOrder(fixture);
-        FlashDeliveryAddressChangeRequest input = addressChange();
-        input.setDelivery(input.getPickup());
-        assertThrows(ServiceException.class, () -> fixture.service.quoteAddress(7L, 10L, input));
-        input.setDelivery(request().getDelivery());
-        input.getPickup().setPhone("");
-        assertThrows(ServiceException.class, () -> fixture.service.quoteAddress(7L, 10L, input));
-        when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).thenReturn(new RouteDistance(40001, 1000, "GOOGLE"));
-        assertThrows(ServiceException.class, () -> fixture.service.quoteAddress(7L, 10L, addressChange()));
-        verify(fixture.orderMapper, never()).updateWaitingAddress(any(), any());
-    }
-
-    @Test
-    void riderAcceptanceBetweenReadAndAddressSaveRejectsWriteWithoutLog() {
-        Fixture fixture = new Fixture();
-        editableOrder(fixture);
-        FlashDeliveryAddressConfirmRequest input = confirmAddress(fixture);
-        when(fixture.orderMapper.updateWaitingAddress(any(), eq(2))).thenReturn(0);
-        ServiceException exception = assertThrows(ServiceException.class,
-                () -> fixture.service.updateAddress(7L, 10L, input));
-        assertEquals("flash.delivery.state.changed", exception.getMessage());
-        verify(fixture.logMapper, never()).insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderLog.class));
-    }
-
-    @Test
-    void addingTipOnlyIncreasesTipAndTotalAndRejectsSameVersionRetry() {
-        Fixture fixture = new Fixture();
-        FlashDeliveryOrder order = editableOrder(fixture);
-        when(fixture.orderMapper.updateWaitingTip(any(), eq(2))).thenReturn(1);
-        var view = fixture.service.addTip(7L, 10L, tipRequest(20L, 2));
-        assertEquals(30L, view.getTipAmount());
-        assertEquals(132L, view.getAmount());
-        assertEquals(102L, view.getBaseDeliveryFee());
-        assertEquals(3200, view.getDistanceMeters());
-        assertEquals(3, view.getOrderVersion());
-        assertThrows(ServiceException.class, () -> fixture.service.addTip(7L, 10L, tipRequest(20L, 2)));
-        assertEquals(30L, order.getTipAmount());
-        verify(fixture.orderMapper).updateWaitingTip(any(), eq(2));
-        verify(fixture.routeService, never()).calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt());
-        verify(fixture.pricingMapper, never()).selectAtTime(anyString(), anyInt());
-    }
-
-    @Test
-    void tipChangeInvalidatesEarlierAddressConfirmation() {
-        Fixture fixture = new Fixture();
-        editableOrder(fixture);
-        FlashDeliveryAddressConfirmRequest confirmation = confirmAddress(fixture);
-        when(fixture.orderMapper.updateWaitingTip(any(), eq(2))).thenReturn(1);
-        fixture.service.addTip(7L, 10L, tipRequest(20L, 2));
-        assertThrows(ServiceException.class, () -> fixture.service.updateAddress(7L, 10L, confirmation));
-        verify(fixture.orderMapper, never()).updateWaitingAddress(any(), any());
-    }
-
-    @Test
-    void additionalTipRejectsFractionalJsonBeforeWriting() throws Exception {
-        Fixture fixture = new Fixture();
-        editableOrder(fixture);
-        when(fixture.orderMapper.updateWaitingTip(any(), eq(2))).thenReturn(1);
-        FlashDeliveryTipAddRequest input = new com.fasterxml.jackson.databind.ObjectMapper().readValue(
-                "{\"orderVersion\":2,\"additionalTipAmount\":1.9}", FlashDeliveryTipAddRequest.class);
-        ServiceException exception = assertThrows(ServiceException.class,
-                () -> fixture.service.addTip(7L, 10L, input));
-        assertEquals("flash.delivery.tip.additional.invalid", exception.getMessage());
-        verify(fixture.orderMapper, never()).updateWaitingTip(any(), any());
-    }
-
-    @Test
-    void additionalTipRejectsJsonAmountBeyondLongRange() throws Exception {
-        Fixture fixture = new Fixture();
-        editableOrder(fixture);
-        FlashDeliveryTipAddRequest input = new com.fasterxml.jackson.databind.ObjectMapper().readValue(
-                "{\"orderVersion\":2,\"additionalTipAmount\":1e100}", FlashDeliveryTipAddRequest.class);
-        ServiceException exception = assertThrows(ServiceException.class,
-                () -> fixture.service.addTip(7L, 10L, input));
-        assertEquals("flash.delivery.tip.additional.invalid", exception.getMessage());
-        verify(fixture.orderMapper, never()).updateWaitingTip(any(), any());
-    }
-
-    @Test
-    void additionalTipRejectsZeroNegativeNullAndOverflow() {
-        Fixture fixture = new Fixture();
-        editableOrder(fixture);
-        for (Long value : java.util.Arrays.asList(null, 0L, -1L, Long.MAX_VALUE)) {
-            assertThrows(ServiceException.class,
-                    () -> fixture.service.addTip(7L, 10L, tipRequest(value, 2)));
-        }
-        verify(fixture.orderMapper, never()).updateWaitingTip(any(), any());
-    }
-
-    @Test
-    void riderAcceptanceBetweenReadAndTipSaveRejectsWriteWithoutLog() {
-        Fixture fixture = new Fixture();
-        editableOrder(fixture);
-        when(fixture.orderMapper.updateWaitingTip(any(), eq(2))).thenReturn(0);
-        assertThrows(ServiceException.class, () -> fixture.service.addTip(7L, 10L, tipRequest(20L, 2)));
-        verify(fixture.logMapper, never()).insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderLog.class));
-    }
-
-    private FlashDeliveryOrder editableOrder(Fixture fixture) {
-        FlashDeliveryOrder order = order(10L, "WAITING_ACCEPTANCE");
-        order.setRiderId(null);
-        order.setVersion(2);
-        order.setPricingId(1L);
-        order.setPricingVersion(1);
-        order.setPricingStartTime("00:00");
-        order.setPricingEndTime("24:00");
-        order.setStartingDistance(new BigDecimal("3.00"));
-        order.setStartingFare(90L);
-        order.setDistance(new BigDecimal("1.00"));
-        order.setFreight(15L);
-        when(fixture.orderMapper.selectById(10L)).thenReturn(order);
-        when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).thenReturn(new RouteDistance(4500, 900, "GOOGLE"));
-        // 后台现价与快照不同;改址仍以订单保存的价格计算。
-        when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
-        return order;
-    }
-
-    private FlashDeliveryAddressChangeRequest addressChange() {
-        FlashDeliveryAddressChangeRequest input = new FlashDeliveryAddressChangeRequest();
-        input.setOrderVersion(2);
-        input.setPickup(request().getPickup());
-        input.setDelivery(request().getDelivery());
-        return input;
-    }
-
-    private FlashDeliveryAddressConfirmRequest confirmAddress(Fixture fixture) {
-        FlashDeliveryAddressChangeRequest change = addressChange();
-        FlashDeliveryQuoteView quote = fixture.service.quoteAddress(7L, 10L, change);
-        FlashDeliveryAddressConfirmRequest input = new FlashDeliveryAddressConfirmRequest();
-        input.setOrderVersion(quote.getOrderVersion());
-        input.setPickup(change.getPickup());
-        input.setDelivery(change.getDelivery());
-        input.setQuotedDistanceMeters(quote.getDistanceMeters());
-        input.setQuotedBaseDeliveryFee(quote.getBaseDeliveryFee());
-        input.setQuotedDistanceFee(quote.getDistanceFee());
-        input.setQuotedUrgentFee(quote.getUrgentFee());
-        input.setQuotedAmount(quote.getAmount());
-        return input;
-    }
-
-    private FlashDeliveryTipAddRequest tipRequest(Long amount, Integer version) {
-        FlashDeliveryTipAddRequest input = new FlashDeliveryTipAddRequest();
-        input.setAdditionalTipAmount(amount == null ? null : BigDecimal.valueOf(amount));
-        input.setOrderVersion(version);
-        return input;
-    }
-
-    @Test
-    void generateOrderNoUsesTimestampAndSequenceFormat() {
-        String first = FlashDeliveryApplicationService.generateOrderNo();
-        String second = FlashDeliveryApplicationService.generateOrderNo();
-
-        assertTrue(isFlashOrderNo(first), "应为 98 前缀 17 位纯数字,实际: " + first);
-        assertTrue(isFlashOrderNo(second), "应为 98 前缀 17 位纯数字,实际: " + second);
-        assertNotEquals(first, second, "同毫秒连续生成应由序列位区分,不得同号");
-    }
-
-    @Test
-    void createRegeneratesOrderNoWhenUniqueKeyConflicts() {
-        Fixture fixture = new Fixture();
-        when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
-        when(fixture.routeService.calculate(any(), any(), anyInt())).thenReturn(new RouteDistance(3200, 600, "ROUTE"));
-        when(fixture.userMapper.selectOrdinaryUserIdsByNormalizedPhone(anyString())).thenReturn(List.of());
-        org.springframework.dao.DataIntegrityViolationException orderNoConflict =
-                new org.springframework.dao.DataIntegrityViolationException("insert failed",
-                        new java.sql.SQLException(
-                                "Duplicate entry '98001' for key 'flash_delivery_order.uk_flash_order_no'"));
-        // 捕获器拿到的是同一对象引用,重试改单号后两次取值相同;改为每次调用时记录当时单号
-        java.util.List<String> attempted = new java.util.ArrayList<>();
-        doAnswer(invocation -> {
-            FlashDeliveryOrder order = invocation.getArgument(0);
-            attempted.add(order.getOrderNo());
-            if (attempted.size() == 1) throw orderNoConflict;
-            order.setId(99L);
-            return 1;
-        }).when(fixture.orderMapper).insert(any(FlashDeliveryOrder.class));
-
-        fixture.service.create(7L, createRequest());
-
-        verify(fixture.orderMapper, times(2)).insert(any(FlashDeliveryOrder.class));
-        assertEquals(2, attempted.size());
-        assertNotEquals(attempted.get(0), attempted.get(1), "撞号重试后应换新单号");
-        assertTrue(isFlashOrderNo(attempted.get(1)), "重试后仍应为新格式单号");
-    }
-
-    @Test
-    void createKeepsIdempotencyFallbackForNonOrderNoConflicts() {
-        Fixture fixture = new Fixture();
-        when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
-        when(fixture.routeService.calculate(any(), any(), anyInt())).thenReturn(new RouteDistance(3200, 600, "ROUTE"));
-        when(fixture.userMapper.selectOrdinaryUserIdsByNormalizedPhone(anyString())).thenReturn(List.of());
-        org.springframework.dao.DataIntegrityViolationException idempotentConflict =
-                new org.springframework.dao.DataIntegrityViolationException("insert failed",
-                        new java.sql.SQLException(
-                                "Duplicate entry '7-req-1' for key 'flash_delivery_order.uk_user_request'"));
-        doThrow(idempotentConflict).when(fixture.orderMapper).insert(any(FlashDeliveryOrder.class));
-        // 前置幂等查询放行(null);插入冲突后 catch 内按 v1 实现走 FOR UPDATE 复查 + lockOrder 锁单
-        FlashDeliveryOrder concurrent = new FlashDeliveryOrder();
-        concurrent.setId(55L);
-        concurrent.setOrderNo("981789000000011");
-        when(fixture.orderMapper.selectByUserRequestId(eq(7L), anyString())).thenReturn(null);
-        when(fixture.orderMapper.selectOne(any(Wrapper.class))).thenReturn(concurrent);
-        when(fixture.orderMapper.selectById(55L)).thenReturn(concurrent);
-
-        var detail = fixture.service.create(7L, createRequest());
-
-        assertEquals(55L, detail.getId());
-        verify(fixture.orderMapper, times(1)).insert(any(FlashDeliveryOrder.class));
-    }
-
-    /** 闪送新格式单号:98 开头、17 位纯数字。 */
-    private static boolean isFlashOrderNo(String orderNo) {
-        return orderNo != null && orderNo.startsWith("98") && orderNo.length() == 17
-                && orderNo.chars().allMatch(Character::isDigit);
-    }
-
-    private static class Fixture {
-        final com.ruoyi.system.mapper.flash.FlashDeliveryOrderStopMapper stopMapper = mock(com.ruoyi.system.mapper.flash.FlashDeliveryOrderStopMapper.class);
-        final com.ruoyi.system.mapper.flash.FlashDeliveryTargetAdjustmentMapper adjustmentMapper = mock(com.ruoyi.system.mapper.flash.FlashDeliveryTargetAdjustmentMapper.class);
-        final FlashDeliveryOrderMapper orderMapper = mock(FlashDeliveryOrderMapper.class);
-        final FlashDeliveryPricingMapper pricingMapper = mock(FlashDeliveryPricingMapper.class);
-        final FlashDeliveryOrderImageMapper imageMapper = mock(FlashDeliveryOrderImageMapper.class);
-        final FlashDeliveryOrderLogMapper logMapper = mock(FlashDeliveryOrderLogMapper.class);
-        final InfoUserMapper userMapper = mock(InfoUserMapper.class);
-        final FlashDeliveryRouteService routeService = mock(FlashDeliveryRouteService.class);
-        final RiderDeliveryLockService lockService = mock(RiderDeliveryLockService.class);
-        final RiderDeliveryAcceptanceService acceptanceService = mock(RiderDeliveryAcceptanceService.class);
-        final com.ruoyi.system.service.PaymentMethodGateService paymentMethodGateService =
-                mock(com.ruoyi.system.service.PaymentMethodGateService.class);
-        final FlashDeliveryNotificationService notificationService = mock(FlashDeliveryNotificationService.class);
-        final FlashDeliveryApplicationService service = new FlashDeliveryApplicationService(
-                orderMapper, pricingMapper, imageMapper, logMapper, userMapper,
-                routeService, new FlashDeliveryPricingCalculator(), lockService, acceptanceService, stopMapper, adjustmentMapper,
-                paymentMethodGateService, notificationService);
-
-        Fixture() {
-            when(paymentMethodGateService.listAvailable(any(), any(), any())).thenReturn(List.of(
-                    new com.ruoyi.system.service.PaymentMethodGateService.PayMethodOption("COD", "1", true),
-                    new com.ruoyi.system.service.PaymentMethodGateService.PayMethodOption("LINE_PAY", "3", true),
-                    new com.ruoyi.system.service.PaymentMethodGateService.PayMethodOption("OFFLINE_TRANSFER", "6", true)));
-            // Existing single-stop scenarios now explicitly provide station rows; production never synthesizes legacy stops.
-            when(orderMapper.selectByIdForUpdate(any())).thenAnswer(i -> {
-                FlashDeliveryOrder current = orderMapper.selectById((Long) i.getArgument(0));
-                if (current != null && current.getPickupStopCount() == null) current.setPickupStopCount(1);
-                return current;
-            });
-            when(stopMapper.insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderStop.class))).thenReturn(1);
-            when(stopMapper.updateById(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderStop.class))).thenReturn(1);
-            when(imageMapper.insert(any(FlashDeliveryOrderImage.class))).thenReturn(1);
-            when(stopMapper.selectList(any())).thenAnswer(i -> {
-                FlashDeliveryOrder current = orderMapper.selectById(10L);
-                var pickup = new com.ruoyi.system.domain.flash.FlashDeliveryOrderStop();
-                pickup.setId(101L); pickup.setOrderId(10L); pickup.setStopType("PICKUP"); pickup.setStopOrder(0);
-                pickup.setName("pickup contact"); pickup.setPhone("0911111111"); pickup.setAddress("pickup road");
-                pickup.setLatitude(new BigDecimal("25.0100")); pickup.setLongitude(new BigDecimal("121.5100"));
-                pickup.setPackageType("DOCUMENT"); pickup.setQuantity(1); pickup.setWeightRange("UP_TO_5_KG");
-                pickup.setStatus(current != null && List.of("PICKED_UP", "DELIVERED", "COMPLETED").contains(current.getStatus())
-                        ? "PICKED_UP" : "PENDING");
-                var delivery = new com.ruoyi.system.domain.flash.FlashDeliveryOrderStop();
-                delivery.setId(102L); delivery.setOrderId(10L); delivery.setStopType("DELIVERY"); delivery.setStopOrder(1); delivery.setStatus("PENDING");
-                return List.of(pickup, delivery);
-            });
-            when(lockService.withLock(any(), any())).thenAnswer(invocation -> {
-                RiderDeliveryLockService.LockedCall<?> call = invocation.getArgument(1);
-                return call.call();
-            });
-        }
-    }
-}
+package com.ruoyi.app.flashdelivery.service;
+
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.ruoyi.app.flashdelivery.dto.FlashDeliveryAddressRequest;
+import com.ruoyi.app.flashdelivery.dto.FlashDeliveryAddressChangeRequest;
+import com.ruoyi.app.flashdelivery.dto.FlashDeliveryAddressConfirmRequest;
+import com.ruoyi.app.flashdelivery.dto.FlashDeliveryTipAddRequest;
+import com.ruoyi.app.flashdelivery.dto.FlashDeliveryQuoteView;
+import com.ruoyi.app.flashdelivery.dto.FlashDeliveryCreateRequest;
+import com.ruoyi.app.flashdelivery.dto.FlashDeliveryProofRequest;
+import com.ruoyi.app.flashdelivery.dto.FlashDeliveryPricingRequest;
+import com.ruoyi.app.flashdelivery.dto.FlashDeliveryQuoteRequest;
+import com.ruoyi.app.flashdelivery.dto.FlashDeliveryReasonRequest;
+import com.ruoyi.app.flashdelivery.dto.FlashDeliveryDeliverRequest;
+import com.ruoyi.app.flashdelivery.dto.FlashDeliveryRiderOrderPageView;
+import com.ruoyi.app.flashdelivery.exception.FlashDeliveryQuoteChangedException;
+import com.ruoyi.app.flashdelivery.route.FlashDeliveryRouteService;
+import com.ruoyi.app.flashdelivery.route.GeoPoint;
+import com.ruoyi.app.flashdelivery.route.RouteDistance;
+import com.ruoyi.app.order.RiderDeliveryAcceptanceService;
+import com.ruoyi.app.order.RiderDeliveryLockService;
+import com.ruoyi.system.domain.InfoUser;
+import com.ruoyi.system.domain.flash.FlashDeliveryOrder;
+import com.ruoyi.system.domain.flash.FlashDeliveryOrderImage;
+import com.ruoyi.system.domain.flash.FlashDeliveryPricing;
+import com.ruoyi.system.mapper.InfoUserMapper;
+import com.ruoyi.system.mapper.flash.FlashDeliveryOrderImageMapper;
+import com.ruoyi.system.mapper.flash.FlashDeliveryOrderLogMapper;
+import com.ruoyi.system.mapper.flash.FlashDeliveryOrderMapper;
+import com.ruoyi.system.mapper.flash.FlashDeliveryPricingMapper;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.AfterAll;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
+import org.mockito.MockedStatic;
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.beans.factory.support.DefaultListableBeanFactory;
+import org.springframework.context.support.StaticMessageSource;
+import com.ruoyi.common.core.domain.entity.SysDictData;
+import com.ruoyi.common.utils.DictUtils;
+import com.ruoyi.common.utils.spring.SpringUtils;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Date;
+import java.util.Calendar;
+
+import static com.ruoyi.system.domain.flash.FlashDeliveryStatus.ACCEPTED;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import com.ruoyi.common.exception.ServiceException;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.verifyNoInteractions;
+
+class FlashDeliveryApplicationServiceTest {
+    private static ConfigurableListableBeanFactory originalBeanFactory;
+
+    @BeforeAll
+    static void initializeMessages() {
+        originalBeanFactory = (ConfigurableListableBeanFactory)
+                ReflectionTestUtils.getField(SpringUtils.class, "beanFactory");
+        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
+        StaticMessageSource messageSource = new StaticMessageSource();
+        messageSource.setUseCodeAsDefaultMessage(true);
+        beanFactory.registerSingleton("messageSource", messageSource);
+        new SpringUtils().postProcessBeanFactory(beanFactory);
+    }
+
+    @AfterAll
+    static void restoreMessages() {
+        new SpringUtils().postProcessBeanFactory(originalBeanFactory);
+    }
+
+    @Test
+    void quoteUsesServerRouteAndCurrentTimePricing() {
+        Fixture fixture = new Fixture();
+        when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
+        when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).thenReturn(new RouteDistance(3001, 600, "ROUTE"));
+
+        var quote = fixture.service.quote(request());
+
+        assertEquals(3001, quote.getDistanceMeters());
+        assertEquals(90L, quote.getAmount());
+        assertEquals(1L, quote.getPricingId());
+        assertEquals("00:00", quote.getStartTime());
+        assertEquals("24:00", quote.getEndTime());
+        assertEquals("ROUTE", quote.getDistanceSource());
+    }
+
+    @Test
+    void quoteRejectsRoutesLongerThanFortyKilometres() {
+        Fixture fixture = new Fixture();
+        when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
+        when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).thenReturn(new RouteDistance(40001, 1800, "ROUTE"));
+
+        assertThrows(ServiceException.class, () -> fixture.service.quote(request()));
+    }
+
+    @Test
+    void wrongDeliveryPinCannotPersistProofOrChangeState() {
+        Fixture fixture = new Fixture();
+        InfoUser rider = new InfoUser();
+        rider.setUserId(8L);
+        rider.setUserType("2");
+        when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
+        FlashDeliveryOrder order = new FlashDeliveryOrder();
+        order.setId(10L);
+        order.setRiderId(8L);
+        order.setStatus("PICKED_UP");
+        order.setPinRequired(true);
+        order.setDeliveryPinCode("4821");
+        when(fixture.orderMapper.selectById(10L)).thenReturn(order);
+        FlashDeliveryDeliverRequest request = new FlashDeliveryDeliverRequest();
+        request.setPinCode("1234");
+        request.setImageUrls(List.of("https://example.com/delivery.jpg"));
+
+        assertThrows(ServiceException.class, () -> fixture.service.deliver(8L, 10L, request));
+        verify(fixture.imageMapper, never()).insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderImage.class));
+        verify(fixture.orderMapper, never()).transitionByRider(any(), any(), any(), any(), any(), any());
+    }
+
+    @Test
+    void correctDeliveryPinPersistsDeliveryProofAndChangesState() {
+        Fixture fixture = new Fixture();
+        InfoUser rider = new InfoUser();
+        rider.setUserId(8L);
+        rider.setUserType("2");
+        when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
+        FlashDeliveryOrder order = new FlashDeliveryOrder();
+        order.setId(10L);
+        order.setRiderId(8L);
+        order.setStatus("PICKED_UP");
+        order.setPinRequired(true);
+        order.setDeliveryPinCode("4821");
+        when(fixture.orderMapper.selectById(10L)).thenReturn(order);
+        when(fixture.orderMapper.transitionByRider(eq(10L), eq(8L), eq("PICKED_UP"),
+                eq("DELIVERED"), eq("delivered_at"), any())).thenReturn(1);
+        FlashDeliveryDeliverRequest request = new FlashDeliveryDeliverRequest();
+        request.setPinCode("4821");
+        request.setImageUrls(List.of("https://example.com/delivery.jpg"));
+
+        fixture.service.deliver(8L, 10L, request);
+
+        ArgumentCaptor<com.ruoyi.system.domain.flash.FlashDeliveryOrderImage> image =
+                ArgumentCaptor.forClass(com.ruoyi.system.domain.flash.FlashDeliveryOrderImage.class);
+        verify(fixture.imageMapper).insert(image.capture());
+        assertEquals("DELIVERY", image.getValue().getProofType());
+        assertEquals("RIDER", image.getValue().getOperatorType());
+    }
+
+    @Test
+    void scheduledCreateGeneratesUserOnlyPinAndStoresRelativeSenderProof() {
+        Fixture fixture = new Fixture();
+        when(fixture.userMapper.selectOrdinaryUserIdsByNormalizedPhone("0922222222"))
+                .thenReturn(List.of(88L));
+        when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
+        when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).thenReturn(new RouteDistance(3200, 600, "ROUTE"));
+        doAnswer(invocation -> {
+            FlashDeliveryOrder order = invocation.getArgument(0);
+            order.setId(99L);
+            return 1;
+        }).when(fixture.orderMapper).insert(any(FlashDeliveryOrder.class));
+        when(fixture.imageMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
+        when(fixture.logMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
+        FlashDeliveryCreateRequest request = createRequest();
+        request.setSenderImageUrls(List.of("/profile/upload/2026/09/07/sender.jpg"));
+
+        var result = fixture.service.create(7L, request);
+
+        assertEquals(4, result.getDeliveryPinCode().length());
+        ArgumentCaptor<com.ruoyi.system.domain.flash.FlashDeliveryOrderImage> image =
+                ArgumentCaptor.forClass(com.ruoyi.system.domain.flash.FlashDeliveryOrderImage.class);
+        verify(fixture.imageMapper).insert(image.capture());
+        assertEquals("SENDER", image.getValue().getProofType());
+        assertEquals("USER", image.getValue().getOperatorType());
+        assertEquals("/profile/upload/2026/09/07/sender.jpg", image.getValue().getImageUrl());
+        ArgumentCaptor<FlashDeliveryOrder> saved = ArgumentCaptor.forClass(FlashDeliveryOrder.class);
+        verify(fixture.orderMapper).insert(saved.capture());
+        assertEquals("SCHEDULED", saved.getValue().getDeliveryMode());
+        assertEquals("NORMAL", saved.getValue().getDeliveryType());
+        assertEquals("DOCUMENT", saved.getValue().getPackageType());
+        assertEquals(1, saved.getValue().getQuantity());
+        assertEquals("UP_TO_5_KG", saved.getValue().getWeightRange());
+        assertEquals("30 x 20 x 10 cm", saved.getValue().getSpecification());
+        assertEquals(88L, saved.getValue().getReceiverUserId());
+        assertEquals(1L, saved.getValue().getPricingId());
+        assertEquals("00:00", saved.getValue().getPricingStartTime());
+        assertEquals("24:00", saved.getValue().getPricingEndTime());
+        assertEquals(new BigDecimal("3.00"), saved.getValue().getStartingDistance());
+        assertEquals(90L, saved.getValue().getStartingFare());
+        assertEquals(new BigDecimal("1.00"), saved.getValue().getDistance());
+        assertEquals(15L, saved.getValue().getFreight());
+        assertEquals(0L, saved.getValue().getDistanceFee());
+        assertEquals(90L, saved.getValue().getBaseDeliveryFee());
+        assertEquals(new BigDecimal("20.00"), saved.getValue().getUrgentRate());
+        assertEquals(10L, saved.getValue().getMinimumUrgentFee());
+        assertEquals(0L, saved.getValue().getUrgentFee());
+        assertEquals(0L, saved.getValue().getTipAmount());
+    }
+
+    @Test
+    void confirmPaymentRequiresDeliveredState() {
+        Fixture fixture = new Fixture();
+        rider(fixture, 9L);
+        FlashDeliveryOrder pickedUp = order(10L, "PICKED_UP");
+        pickedUp.setRiderId(9L);
+        when(fixture.orderMapper.selectById(10L)).thenReturn(pickedUp);
+
+        assertThrows(ServiceException.class, () -> fixture.service.confirmPayment(9L, 10L));
+        verify(fixture.orderMapper, never()).update(any(), any());
+    }
+
+    @Test
+    void confirmPaymentMarksPaidAndWritesLog() {
+        Fixture fixture = new Fixture();
+        rider(fixture, 9L);
+        FlashDeliveryOrder delivered = order(10L, "DELIVERED");
+        delivered.setRiderId(9L);
+        when(fixture.orderMapper.selectById(10L)).thenReturn(delivered);
+        when(fixture.orderMapper.update(any(), any())).thenReturn(1);
+        when(fixture.logMapper.insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderLog.class))).thenReturn(1);
+
+        fixture.service.confirmPayment(9L, 10L);
+
+        verify(fixture.orderMapper).update(any(), any());
+        ArgumentCaptor<com.ruoyi.system.domain.flash.FlashDeliveryOrderLog> logCaptor =
+                ArgumentCaptor.forClass(com.ruoyi.system.domain.flash.FlashDeliveryOrderLog.class);
+        verify(fixture.logMapper).insert(logCaptor.capture());
+        assertEquals("PAYMENT_CONFIRMED", logCaptor.getValue().getReason());
+    }
+
+    @Test
+    void confirmPaymentIsIdempotentOnceConfirmed() {
+        Fixture fixture = new Fixture();
+        rider(fixture, 9L);
+        FlashDeliveryOrder confirmed = order(10L, "DELIVERED");
+        confirmed.setRiderId(9L);
+        confirmed.setPaymentStatus(1);
+        when(fixture.orderMapper.selectById(10L)).thenReturn(confirmed);
+
+        fixture.service.confirmPayment(9L, 10L);
+
+        verify(fixture.orderMapper, never()).update(any(), any());
+        verify(fixture.logMapper, never()).insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderLog.class));
+    }
+
+    @Test
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    void userListOnlyFiltersByOwnerAndReturnsCardFields() {
+        Fixture fixture = new Fixture();
+        FlashDeliveryOrder order = order(10L, "COMPLETED");
+        order.setWeightRange("OVER_5_TO_10_KG");
+        Page<FlashDeliveryOrder> source = new Page<>(1, 10, 1);
+        source.setRecords(List.of(order));
+        when(fixture.orderMapper.selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
+                any(Wrapper.class))).thenReturn(source);
+
+        var result = fixture.service.userOrders(7L, 1, 10, null);
+
+        ArgumentCaptor<com.baomidou.mybatisplus.core.conditions.query.QueryWrapper> query =
+                ArgumentCaptor.forClass(com.baomidou.mybatisplus.core.conditions.query.QueryWrapper.class);
+        verify(fixture.orderMapper).selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class), query.capture());
+        String sql = query.getValue().getSqlSegment().toLowerCase();
+        var parameters = query.getValue().getParamNameValuePairs().values();
+        assertTrue(sql.contains("user_id"));
+        assertFalse(sql.contains("status"));
+        assertFalse(sql.contains("service_type"));
+        assertTrue(parameters.contains(7L));
+        assertEquals("pickup road", result.getRecords().get(0).getPickupAddress());
+        assertEquals("OVER_5_TO_10_KG", result.getRecords().get(0).getWeightRange());
+        assertEquals(112L, result.getRecords().get(0).getAmount());
+    }
+
+    @Test
+    void createDoesNotBindReceiverWhenNormalizedPhoneMatchesMultipleUsers() {
+        Fixture fixture = new Fixture();
+        when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
+        when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).thenReturn(new RouteDistance(3200, 600, "ROUTE"));
+        when(fixture.userMapper.selectOrdinaryUserIdsByNormalizedPhone("886912345678"))
+                .thenReturn(List.of(88L, 99L));
+        doAnswer(invocation -> {
+            FlashDeliveryOrder order = invocation.getArgument(0);
+            order.setId(99L);
+            return 1;
+        }).when(fixture.orderMapper).insert(any(FlashDeliveryOrder.class));
+        FlashDeliveryCreateRequest request = createRequest();
+        request.getDelivery().setPhone("+886 912-345-678");
+
+        fixture.service.create(7L, request);
+
+        ArgumentCaptor<FlashDeliveryOrder> saved = ArgumentCaptor.forClass(FlashDeliveryOrder.class);
+        verify(fixture.orderMapper).insert(saved.capture());
+        assertNull(saved.getValue().getReceiverUserId());
+    }
+
+    @Test
+    void createLeavesReceiverUnboundWhenPhoneHasNoRegisteredUser() {
+        Fixture fixture = new Fixture();
+        when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
+        when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).thenReturn(new RouteDistance(3200, 600, "ROUTE"));
+        when(fixture.userMapper.selectOrdinaryUserIdsByNormalizedPhone("0922222222"))
+                .thenReturn(List.of());
+        doAnswer(invocation -> {
+            FlashDeliveryOrder order = invocation.getArgument(0);
+            order.setId(99L);
+            return 1;
+        }).when(fixture.orderMapper).insert(any(FlashDeliveryOrder.class));
+
+        fixture.service.create(7L, createRequest());
+
+        ArgumentCaptor<FlashDeliveryOrder> saved = ArgumentCaptor.forClass(FlashDeliveryOrder.class);
+        verify(fixture.orderMapper).insert(saved.capture());
+        assertNull(saved.getValue().getReceiverUserId());
+    }
+
+    @Test
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    void receiverListFiltersByCreationTimeReceiverBinding() {
+        Fixture fixture = new Fixture();
+        when(fixture.orderMapper.selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
+                any(Wrapper.class))).thenReturn(new Page<FlashDeliveryOrder>());
+
+        fixture.service.userOrders(88L, 1, 10, "receiver");
+
+        ArgumentCaptor<com.baomidou.mybatisplus.core.conditions.query.QueryWrapper> query =
+                ArgumentCaptor.forClass(com.baomidou.mybatisplus.core.conditions.query.QueryWrapper.class);
+        verify(fixture.orderMapper).selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class), query.capture());
+        assertTrue(query.getValue().getSqlSegment().toLowerCase().contains("receiver_user_id"));
+        assertTrue(query.getValue().getParamNameValuePairs().values().contains(88L));
+    }
+
+    @Test
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    void riderNewTaskUsesFoodOrderParametersAndReturnsPrototypeCardSummary() {
+        Fixture fixture = new Fixture();
+        rider(fixture, 8L);
+        FlashDeliveryOrder order = order(10L, "WAITING_ACCEPTANCE");
+        order.setWeightRange("OVER_10_TO_15_KG");
+        Page<FlashDeliveryOrder> source = new Page<>(1, 10, 1);
+        source.setRecords(List.of(order));
+        when(fixture.orderMapper.selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
+                any(Wrapper.class))).thenReturn(source);
+        when(fixture.orderMapper.selectObjs(any(Wrapper.class))).thenReturn(List.of(168L));
+
+        SysDictData distanceLimit = new SysDictData();
+        distanceLimit.setDictValue("5");
+        FlashDeliveryRiderOrderPageView result;
+        try (MockedStatic<DictUtils> dictionary = mockStatic(DictUtils.class)) {
+            dictionary.when(() -> DictUtils.getDictCache("sys_qs_newtask_distance"))
+                    .thenReturn(List.of(distanceLimit));
+            result = fixture.service.riderOrders(8L, 1, 10, "newTask",
+                    new BigDecimal("121.5000"), new BigDecimal("25.0000"));
+        }
+
+        assertEquals(1L, result.getNearbyTaskCount());
+        assertEquals(168L, result.getHighestOrderAmount());
+        assertEquals("pickup road", result.getRecords().get(0).getPickupAddress());
+        assertEquals("delivery road", result.getRecords().get(0).getDeliveryAddress());
+        assertEquals("OVER_10_TO_15_KG", result.getRecords().get(0).getWeightRange());
+        assertTrue(result.getRecords().get(0).getPickupDistanceMeters() > 0);
+        assertTrue(result.getRecords().get(0).getPinRequired());
+        ArgumentCaptor<com.baomidou.mybatisplus.core.conditions.query.QueryWrapper> query =
+                ArgumentCaptor.forClass(com.baomidou.mybatisplus.core.conditions.query.QueryWrapper.class);
+        verify(fixture.orderMapper).selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class), query.capture());
+        String sql = query.getValue().getSqlSegment().toLowerCase();
+        var parameters = query.getValue().getParamNameValuePairs().values();
+        assertTrue(sql.contains("status"));
+        assertTrue(sql.contains("rider_id is null"));
+        assertTrue(sql.contains("is_display"));
+        assertTrue(sql.contains("vehicle_type"));
+        assertTrue(sql.contains("scheduled_pickup_start_at"));
+        assertTrue(sql.contains("scheduled_pickup_end_at"));
+        assertTrue(sql.contains("st_distance_sphere"));
+        assertTrue(parameters.contains("WAITING_ACCEPTANCE"));
+        assertTrue(parameters.contains("NOW"));
+        assertTrue(parameters.stream().anyMatch(Date.class::isInstance));
+
+        ArgumentCaptor<com.baomidou.mybatisplus.core.conditions.query.QueryWrapper> summaryQuery =
+                ArgumentCaptor.forClass(com.baomidou.mybatisplus.core.conditions.query.QueryWrapper.class);
+        verify(fixture.orderMapper).selectObjs(summaryQuery.capture());
+        String summarySql = summaryQuery.getValue().getSqlSegment().toLowerCase();
+        var summaryParameters = summaryQuery.getValue().getParamNameValuePairs().values();
+        assertTrue(summarySql.contains("rider_id is null"));
+        assertTrue(summarySql.contains("scheduled_pickup_start_at"));
+        assertTrue(summarySql.contains("scheduled_pickup_end_at"));
+        assertTrue(summarySql.contains("st_distance_sphere"));
+        assertTrue(summaryParameters.contains("WAITING_ACCEPTANCE"));
+    }
+
+    @Test
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    void riderNewTaskWithoutPositionFallsBackToNewestFirst() {
+        Fixture fixture = new Fixture();
+        rider(fixture, 8L);
+        when(fixture.orderMapper.selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
+                any(Wrapper.class))).thenReturn(new Page<FlashDeliveryOrder>());
+        when(fixture.orderMapper.selectObjs(any(Wrapper.class))).thenReturn(List.of());
+
+        fixture.service.riderOrders(8L, 1, 10, "newTask", null, null);
+
+        ArgumentCaptor<com.baomidou.mybatisplus.core.conditions.query.QueryWrapper> query =
+                ArgumentCaptor.forClass(com.baomidou.mybatisplus.core.conditions.query.QueryWrapper.class);
+        verify(fixture.orderMapper).selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class), query.capture());
+        String sql = query.getValue().getSqlSegment().toLowerCase();
+        assertTrue(sql.contains("order by create_time desc"));
+        assertFalse(sql.contains("st_distance_sphere"));
+    }
+
+    @Test
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    void riderTabsMapStatusesBeforePagingAndRestrictAssignedTabsToRider() {
+        assertRiderTab("toPickup", List.of("ACCEPTED"));
+        assertRiderTab("delivering", List.of("PICKED_UP"));
+        assertRiderTab("completed", List.of("DELIVERED", "COMPLETED"));
+        assertRiderTab("cancelled", List.of("CANCELLED"));
+    }
+
+    @Test
+    void riderListRejectsUnknownTabAndUnpairedOrInvalidCoordinates() {
+        Fixture fixture = new Fixture();
+        rider(fixture, 8L);
+
+        assertThrows(ServiceException.class,
+                () -> fixture.service.riderOrders(8L, 1, 10, "refund", null, null));
+        assertThrows(ServiceException.class,
+                () -> fixture.service.riderOrders(8L, 1, 10, null, null, null));
+        assertThrows(ServiceException.class,
+                () -> fixture.service.riderOrders(8L, 1, 10, "newTask", BigDecimal.ONE, null));
+        assertThrows(ServiceException.class,
+                () -> fixture.service.riderOrders(8L, 1, 10, "newTask",
+                        new BigDecimal("181"), BigDecimal.ZERO));
+        verify(fixture.orderMapper, never()).selectPage(any(), any());
+    }
+
+    @Test
+    void flashNewTaskListAndAcceptRequireFlashDeliveryType() {
+        Fixture fixture = new Fixture();
+        InfoUser rider = new InfoUser();
+        rider.setUserId(8L);
+        rider.setUserType("2");
+        rider.setDeliveryType("FOOD");
+        when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
+
+        assertThrows(ServiceException.class,
+                () -> fixture.service.riderOrders(8L, 1, 10, "newTask", null, null));
+        assertThrows(ServiceException.class, () -> fixture.service.accept(8L, 10L));
+        verify(fixture.orderMapper, never()).selectPage(any(), any());
+        verify(fixture.orderMapper, never()).accept(any(), any(), any());
+    }
+
+    @Test
+    void riderDetailHidesWaitingFlashOrdersFromNonFlashRiders() {
+        Fixture fixture = new Fixture();
+        InfoUser rider = new InfoUser();
+        rider.setUserId(8L);
+        rider.setUserType("2");
+        rider.setDeliveryType("FOOD");
+        when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
+        when(fixture.orderMapper.selectById(10L)).thenReturn(order(10L, "WAITING_ACCEPTANCE"));
+
+        assertThrows(ServiceException.class, () -> fixture.service.riderDetail(8L, 10L));
+        verify(fixture.imageMapper, never()).selectList(any());
+    }
+
+    @Test
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    void enabledFlashRiderStillListsNewTasks() {
+        Fixture fixture = new Fixture();
+        InfoUser rider = new InfoUser();
+        rider.setUserId(8L);
+        rider.setUserType("2");
+        rider.setDeliveryType("FLASH");
+        when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
+        when(fixture.orderMapper.selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
+                any(Wrapper.class))).thenReturn(new Page<FlashDeliveryOrder>());
+
+        fixture.service.riderOrders(8L, 1, 10, "newTask", null, null);
+        verify(fixture.orderMapper).selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
+                any(Wrapper.class));
+    }
+
+    @Test
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    void ownOrderTabsRemainAccessibleRegardlessOfDeliveryType() {
+        Fixture fixture = new Fixture();
+        InfoUser rider = new InfoUser();
+        rider.setUserId(8L);
+        rider.setUserType("2");
+        rider.setDeliveryType("FOOD");
+        when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
+        when(fixture.orderMapper.selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
+                any(Wrapper.class))).thenReturn(new Page<FlashDeliveryOrder>());
+
+        fixture.service.riderOrders(8L, 1, 10, "toPickup", null, null);
+        verify(fixture.orderMapper).selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
+                any(Wrapper.class));
+    }
+
+    @Test
+    void assignedRiderTabsIgnoreLocationParameters() {
+        Fixture fixture = new Fixture();
+        rider(fixture, 8L);
+        when(fixture.orderMapper.selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
+                any(Wrapper.class))).thenReturn(new Page<FlashDeliveryOrder>());
+
+        fixture.service.riderOrders(8L, 1, 10, "toPickup",
+                new BigDecimal("181"), new BigDecimal("91"));
+
+        verify(fixture.orderMapper).selectPage(any(), any());
+    }
+
+    @Test
+    void preAcceptRiderDetailIsDirectAndShowsTextAddressWithoutSensitiveFields() {
+        Fixture fixture = new Fixture();
+        rider(fixture, 8L);
+        FlashDeliveryOrder order = order(10L, "WAITING_ACCEPTANCE");
+        order.setWeightRange("OVER_15_TO_20_KG");
+        when(fixture.orderMapper.selectById(10L)).thenReturn(order);
+
+        var result = fixture.service.riderDetail(8L, 10L);
+
+        assertEquals(10L, result.getId());
+        assertEquals("pickup road", result.getPickup().getAddress());
+        assertEquals("pickup door", result.getPickup().getAddressDetail());
+        assertEquals("delivery road", result.getDelivery().getAddress());
+        assertEquals("delivery door", result.getDelivery().getAddressDetail());
+        assertEquals("OVER_15_TO_20_KG", result.getWeightRange());
+        assertNull(result.getPickup().getName());
+        assertNull(result.getPickup().getPhone());
+        assertNull(result.getPickup().getLongitude());
+        assertNull(result.getPickup().getLatitude());
+        assertNull(result.getDelivery().getName());
+        assertNull(result.getDelivery().getPhone());
+        assertNull(result.getDelivery().getLongitude());
+        assertNull(result.getDelivery().getLatitude());
+        assertNull(result.getDeliveryPinCode());
+        assertNull(result.getUserNote());
+        assertNull(result.getUser());
+        assertTrue(result.getSenderImageUrls().isEmpty());
+        assertTrue(result.getPickupImageUrls().isEmpty());
+        assertTrue(result.getDeliveryImageUrls().isEmpty());
+        verify(fixture.userMapper, never()).selectInfoUserByUserId(7L);
+        verify(fixture.imageMapper, never()).selectList(any());
+        verify(fixture.logMapper, never()).selectList(any());
+    }
+
+    @Test
+    void userDetailReturnsDirectOrderAndGroupsImageUrlsWithoutRawLogs() {
+        Fixture fixture = new Fixture();
+        FlashDeliveryOrder order = order(10L, ACCEPTED);
+        order.setRiderId(null);
+        order.setWeightRange("OVER_5_TO_10_KG");
+        when(fixture.orderMapper.selectById(10L)).thenReturn(order);
+        when(fixture.imageMapper.selectList(any(Wrapper.class))).thenReturn(List.of(
+                image("SENDER", "https://example.com/sender.jpg"),
+                image("PICKUP", "https://example.com/pickup.jpg"),
+                image("DELIVERY", "https://example.com/delivery.jpg")));
+
+        var result = fixture.service.userDetail(7L, 10L);
+
+        assertEquals(10L, result.getId());
+        assertEquals(List.of("https://example.com/sender.jpg"), result.getSenderImageUrls());
+        assertEquals(List.of("https://example.com/pickup.jpg"), result.getPickupImageUrls());
+        assertEquals(List.of("https://example.com/delivery.jpg"), result.getDeliveryImageUrls());
+        assertEquals("4821", result.getDeliveryPinCode());
+        assertEquals("OVER_5_TO_10_KG", result.getWeightRange());
+        verify(fixture.logMapper, never()).selectList(any());
+    }
+
+    @Test
+    void userDetailReturnsRiderInfoAfterAcceptance() {
+        Fixture fixture = new Fixture();
+        FlashDeliveryOrder order = order(10L, ACCEPTED);
+        order.setRiderId(8L);
+        when(fixture.orderMapper.selectById(10L)).thenReturn(order);
+        InfoUser rider = new InfoUser();
+        rider.setUserId(8L);
+        rider.setUserType("2");
+        rider.setNickName("闪送骑士");
+        rider.setTelPhone("0933333333");
+        rider.setVehicleType("2");
+        rider.setLicensePlate("ABC-1234");
+        when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
+
+        var result = fixture.service.userDetail(7L, 10L);
+
+        assertEquals(8L, result.getRider().getUserId());
+        assertEquals("闪送骑士", result.getRider().getNickName());
+        assertEquals("0933333333", result.getRider().getTelPhone());
+        assertEquals("ABC-1234", result.getRider().getLicensePlate());
+        assertNull(result.getUser());
+    }
+
+    @Test
+    void assignedRiderDetailReturnsSenderInfo() {
+        Fixture fixture = new Fixture();
+        rider(fixture, 8L);
+        FlashDeliveryOrder order = order(10L, ACCEPTED);
+        order.setRiderId(8L);
+        when(fixture.orderMapper.selectById(10L)).thenReturn(order);
+        InfoUser sender = new InfoUser();
+        sender.setUserId(7L);
+        sender.setNickName("寄件达人");
+        sender.setPhone("0911111111");
+        sender.setAvatar("https://example.com/avatar.png");
+        when(fixture.userMapper.selectInfoUserByUserId(7L)).thenReturn(sender);
+
+        var result = fixture.service.riderDetail(8L, 10L);
+
+        assertEquals(7L, result.getUser().getUserId());
+        assertEquals("寄件达人", result.getUser().getNickName());
+        assertEquals("0911111111", result.getUser().getPhone());
+        assertEquals("https://example.com/avatar.png", result.getUser().getAvatar());
+        assertNull(result.getRider());
+    }
+
+    @Test
+    void matchedReceiverCanViewAndConfirmButCannotCancelOrder() {
+        Fixture fixture = new Fixture();
+        FlashDeliveryOrder order = order(10L, "DELIVERED");
+        order.setReceiverUserId(88L);
+        when(fixture.orderMapper.selectById(10L)).thenReturn(order);
+        when(fixture.orderMapper.transitionByParticipant(eq(10L), eq(88L), eq("DELIVERED"),
+                eq("COMPLETED"), any(Date.class))).thenReturn(1);
+        when(fixture.imageMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
+
+        assertDoesNotThrow(() -> fixture.service.userDetail(88L, 10L));
+        assertDoesNotThrow(() -> fixture.service.confirmReceipt(88L, 10L));
+        assertThrows(ServiceException.class,
+                () -> fixture.service.userCancel(88L, 10L, new com.ruoyi.app.flashdelivery.dto.FlashDeliveryReasonRequest()));
+        verify(fixture.orderMapper).transitionByParticipant(eq(10L), eq(88L), eq("DELIVERED"),
+                eq("COMPLETED"), any(Date.class));
+    }
+
+    @Test
+    void senderRatesRiderOnCompletedOrder() {
+        Fixture fixture = new Fixture();
+        FlashDeliveryOrder order = order(10L, "COMPLETED");
+        order.setRiderId(8L);
+        when(fixture.orderMapper.selectById(10L)).thenReturn(order);
+        when(fixture.orderMapper.rateRider(eq(10L), eq(7L), eq(4.5), any(Date.class))).thenReturn(1);
+
+        fixture.service.rateRider(7L, 10L, rating(4.5));
+
+        verify(fixture.orderMapper).rateRider(eq(10L), eq(7L), eq(4.5), any(Date.class));
+    }
+
+    @Test
+    void rateRiderRejectsUserOtherThanSender() {
+        Fixture fixture = new Fixture();
+        FlashDeliveryOrder order = order(10L, "COMPLETED");
+        order.setRiderId(8L);
+        when(fixture.orderMapper.selectById(10L)).thenReturn(order);
+
+        assertThrows(ServiceException.class, () -> fixture.service.rateRider(88L, 10L, rating(4.5)));
+        verify(fixture.orderMapper, never()).rateRider(any(), any(), any(), any());
+    }
+
+    @Test
+    void rateRiderRejectsOrderNotCompletedOrWithoutRider() {
+        Fixture fixture = new Fixture();
+        FlashDeliveryOrder delivered = order(10L, "DELIVERED");
+        delivered.setRiderId(8L);
+        when(fixture.orderMapper.selectById(10L)).thenReturn(delivered);
+        assertThrows(ServiceException.class, () -> fixture.service.rateRider(7L, 10L, rating(4.5)));
+
+        FlashDeliveryOrder completedWithoutRider = order(11L, "COMPLETED");
+        when(fixture.orderMapper.selectById(11L)).thenReturn(completedWithoutRider);
+        assertThrows(ServiceException.class, () -> fixture.service.rateRider(7L, 11L, rating(4.5)));
+
+        verify(fixture.orderMapper, never()).rateRider(any(), any(), any(), any());
+    }
+
+    @Test
+    void rateRiderRejectsAlreadyRatedOrder() {
+        Fixture fixture = new Fixture();
+        FlashDeliveryOrder order = order(10L, "COMPLETED");
+        order.setRiderId(8L);
+        order.setRiderStars(4.5);
+        when(fixture.orderMapper.selectById(10L)).thenReturn(order);
+
+        assertThrows(ServiceException.class, () -> fixture.service.rateRider(7L, 10L, rating(3.0)));
+        verify(fixture.orderMapper, never()).rateRider(any(), any(), any(), any());
+    }
+
+    @Test
+    void rateRiderRejectsConcurrentDuplicateSubmission() {
+        Fixture fixture = new Fixture();
+        FlashDeliveryOrder order = order(10L, "COMPLETED");
+        order.setRiderId(8L);
+        when(fixture.orderMapper.selectById(10L)).thenReturn(order);
+        when(fixture.orderMapper.rateRider(eq(10L), eq(7L), eq(4.5), any(Date.class))).thenReturn(0);
+
+        assertThrows(ServiceException.class, () -> fixture.service.rateRider(7L, 10L, rating(4.5)));
+    }
+
+    @Test
+    void rateRiderRejectsMissingOrOutOfRangeStars() {
+        Fixture fixture = new Fixture();
+
+        assertThrows(ServiceException.class, () -> fixture.service.rateRider(7L, 10L, null));
+        assertThrows(ServiceException.class, () -> fixture.service.rateRider(7L, 10L, rating(null)));
+        assertThrows(ServiceException.class, () -> fixture.service.rateRider(7L, 10L, rating(0.5)));
+        assertThrows(ServiceException.class, () -> fixture.service.rateRider(7L, 10L, rating(5.5)));
+
+        verify(fixture.orderMapper, never()).selectById(any());
+        verify(fixture.orderMapper, never()).rateRider(any(), any(), any(), any());
+    }
+
+    @Test
+    void userDetailReturnsRiderActiveCardForDeliveredOfflineOrder() {
+        Fixture fixture = new Fixture();
+        FlashDeliveryOrder order = order(10L, "DELIVERED");
+        order.setRiderId(8L);
+        order.setPayType("6");
+        when(fixture.orderMapper.selectById(10L)).thenReturn(order);
+        rider(fixture, 8L);
+        com.ruoyi.system.domain.InfoBankCard card = new com.ruoyi.system.domain.InfoBankCard();
+        card.setBankName("中國信託");
+        card.setAccountNo("1234567890");
+        card.setAccountName("王大明");
+        when(fixture.bankCardMapper.selectList(any(Wrapper.class))).thenReturn(java.util.List.of(card));
+
        var result = fixture.service.userDetail(7L, 10L);
+
        assertNotNull(result.getRiderBankCard());
+        assertEquals("中國信託", result.getRiderBankCard().getBankName());
+        assertEquals("1234567890", result.getRiderBankCard().getAccountNo());
+        assertEquals("王大明", result.getRiderBankCard().getAccountName());
+    }
+
+    @Test
+    void userDetailHidesRiderCardBeforeDeliveryOrForNonOfflineOrder() {
+        Fixture fixture = new Fixture();
+        rider(fixture, 8L);
+        // 到付单:已送达也不返回卡
+        FlashDeliveryOrder cod = order(10L, "DELIVERED");
+        cod.setRiderId(8L);
+        cod.setPayType("1");
+        when(fixture.orderMapper.selectById(10L)).thenReturn(cod);
+        assertNull(fixture.service.userDetail(7L, 10L).getRiderBankCard());
+        // 线下转账单:未送达不返回卡
+        FlashDeliveryOrder notDelivered = order(10L, "PICKED_UP");
+        notDelivered.setRiderId(8L);
+        notDelivered.setPayType("6");
+        when(fixture.orderMapper.selectById(10L)).thenReturn(notDelivered);
+        assertNull(fixture.service.userDetail(7L, 10L).getRiderBankCard());
+        // 条件不满足时连卡查询都不发起
+        verify(fixture.bankCardMapper, never()).selectList(any(Wrapper.class));
+    }
+
+    @Test
+    void userDetailHidesRiderCardWhenRiderHasNoActiveCard() {
+        Fixture fixture = new Fixture();
+        FlashDeliveryOrder order = order(10L, "DELIVERED");
+        order.setRiderId(8L);
+        order.setPayType("6");
+        when(fixture.orderMapper.selectById(10L)).thenReturn(order);
+        rider(fixture, 8L);
+        when(fixture.bankCardMapper.selectList(any(Wrapper.class))).thenReturn(java.util.List.of());
+
        assertNull(fixture.service.userDetail(7L, 10L).getRiderBankCard());
+    }
+
+    @Test
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    void riderPoolPassesRiderIdentityAndExcludesNotReadyOptions() {
+        Fixture fixture = new Fixture();
+        rider(fixture, 8L);
+        // 闸门按骑手身份判定就绪度:无启用卡时线下转账 ready=false
+        when(fixture.paymentMethodGateService.listAvailable(
+                eq(com.ruoyi.system.service.PaymentMethodGateService.GateScope.RIDER_FLASH), isNull(), eq(8L)))
+                .thenReturn(java.util.List.of(
+                        new com.ruoyi.system.service.PaymentMethodGateService.PayMethodOption("COD", "1", true),
+                        new com.ruoyi.system.service.PaymentMethodGateService.PayMethodOption("OFFLINE_TRANSFER", "6", false)));
+        when(fixture.orderMapper.selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class), any(Wrapper.class)))
+                .thenReturn(new Page<FlashDeliveryOrder>());
+        when(fixture.orderMapper.selectObjs(any(Wrapper.class))).thenReturn(java.util.List.of(168L));
+        SysDictData distanceLimit = new SysDictData();
+        distanceLimit.setDictValue("5");
+        try (MockedStatic<DictUtils> dictionary = mockStatic(DictUtils.class)) {
+            dictionary.when(() -> DictUtils.getDictCache("sys_qs_newtask_distance"))
+                    .thenReturn(java.util.List.of(distanceLimit));
+            fixture.service.riderOrders(8L, 1, 10, "newTask", new BigDecimal("121.5000"), new BigDecimal("25.0000"));
+        }
+        // 抢单过滤必须把骑手身份传给闸门(就绪度=骑手启用卡由闸门判定并单测覆盖)
+        verify(fixture.paymentMethodGateService).listAvailable(
+                eq(com.ruoyi.system.service.PaymentMethodGateService.GateScope.RIDER_FLASH), isNull(), eq(8L));
+        // 就绪度过滤后仍生成 pay_type IN 条件(OFFLINE ready=false 不进集合)
+        ArgumentCaptor<com.baomidou.mybatisplus.core.conditions.query.QueryWrapper> query =
+                ArgumentCaptor.forClass(com.baomidou.mybatisplus.core.conditions.query.QueryWrapper.class);
+        verify(fixture.orderMapper).selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class), query.capture());
+        String sql = query.getValue().getSqlSegment();
+        assertTrue(sql.contains("pay_type IN"));
+        // 两个 IN 占位符:现金4 + 到付1(6 被 ready=false 过滤掉)
+        String segment = sql.substring(sql.indexOf("pay_type IN"));
+        String inside = segment.substring(0, segment.indexOf(')'));
+        assertEquals(2, inside.split(java.util.regex.Pattern.quote("#{")).length - 1);
+    }
+
+    @Test
+    void userDetailReturnsRiderStars() {
+        Fixture fixture = new Fixture();
+        FlashDeliveryOrder order = order(10L, "COMPLETED");
+        order.setRiderId(8L);
+        order.setRiderStars(4.5);
+        when(fixture.orderMapper.selectById(10L)).thenReturn(order);
+        when(fixture.imageMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
+
+        var view = fixture.service.userDetail(7L, 10L);
+
+        assertEquals(4.5, view.getRiderStars());
+    }
+
+    @Test
+    void userDetailRiderCarriesFlashAverageStars() {
+        Fixture fixture = new Fixture();
+        FlashDeliveryOrder order = order(10L, "ACCEPTED");
+        order.setRiderId(8L);
+        when(fixture.orderMapper.selectById(10L)).thenReturn(order);
+        InfoUser rider = new InfoUser();
+        rider.setUserId(8L);
+        rider.setNickName("闪送骑手");
+        when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
+        when(fixture.orderMapper.selectRiderAverageStars(8L)).thenReturn(4.8);
+        when(fixture.imageMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
+
+        var view = fixture.service.userDetail(7L, 10L);
+
+        assertEquals(4.8, view.getRider().getStar());
+    }
+
+    @Test
+    void userDetailRiderFallsBackToDefaultStarsWithoutRatings() {
+        Fixture fixture = new Fixture();
+        FlashDeliveryOrder order = order(10L, "ACCEPTED");
+        order.setRiderId(8L);
+        when(fixture.orderMapper.selectById(10L)).thenReturn(order);
+        InfoUser rider = new InfoUser();
+        rider.setUserId(8L);
+        when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
+        when(fixture.orderMapper.selectRiderAverageStars(8L)).thenReturn(null);
+        when(fixture.imageMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
+
+        var view = fixture.service.userDetail(7L, 10L);
+
+        assertEquals(4.5, view.getRider().getStar());
+    }
+
+    @Test
+    void userDetailByOrderNoReturnsParticipantDetail() {
+        Fixture fixture = new Fixture();
+        FlashDeliveryOrder order = order(10L, "COMPLETED");
+        order.setRiderId(8L);
+        order.setRiderStars(4.5);
+        when(fixture.orderMapper.selectOne(any(Wrapper.class))).thenReturn(order);
+        InfoUser rider = new InfoUser();
+        rider.setUserId(8L);
+        when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
+        when(fixture.imageMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
+
+        var view = fixture.service.userDetailByOrderNo(7L, "FL-20260813-021");
+
+        assertEquals(10L, view.getId());
+        assertEquals(4.5, view.getRiderStars());
+    }
+
+    @Test
+    void userDetailByOrderNoRejectsNonParticipantAndUnknownOrderNo() {
+        Fixture fixture = new Fixture();
+        FlashDeliveryOrder order = order(10L, "COMPLETED");
+        order.setReceiverUserId(null);
+        when(fixture.orderMapper.selectOne(any(Wrapper.class))).thenReturn(order);
+        assertThrows(ServiceException.class,
+                () -> fixture.service.userDetailByOrderNo(88L, "FL-20260813-021"));
+
+        when(fixture.orderMapper.selectOne(any(Wrapper.class))).thenReturn(null);
+        assertThrows(ServiceException.class,
+                () -> fixture.service.userDetailByOrderNo(7L, "FL-UNKNOWN"));
+    }
+
+    @Test
+    void riderDetailByOrderNoReturnsAssignedOrderDetail() {
+        Fixture fixture = new Fixture();
+        rider(fixture, 8L);
+        FlashDeliveryOrder order = order(10L, "ACCEPTED");
+        order.setRiderId(8L);
+        when(fixture.orderMapper.selectOne(any(Wrapper.class))).thenReturn(order);
+        when(fixture.userMapper.selectInfoUserByUserId(7L)).thenReturn(new InfoUser());
+        when(fixture.imageMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
+
+        var view = fixture.service.riderDetailByOrderNo(8L, "FL-20260813-021");
+
+        assertEquals(10L, view.getId());
+        assertNotNull(view.getUser());
+    }
+
+    @Test
+    void riderDetailByOrderNoRejectsUnknownOrderNo() {
+        Fixture fixture = new Fixture();
+        rider(fixture, 8L);
+        when(fixture.orderMapper.selectOne(any(Wrapper.class))).thenReturn(null);
+
+        assertThrows(ServiceException.class,
+                () -> fixture.service.riderDetailByOrderNo(8L, "FL-UNKNOWN"));
+    }
+
+    @Test
+    void orderWithoutCreationTimeReceiverBindingIsNotClaimedLater() {
+        Fixture fixture = new Fixture();
+        FlashDeliveryOrder historicalOrder = order(10L, "DELIVERED");
+        historicalOrder.setReceiverUserId(null);
+        when(fixture.orderMapper.selectById(10L)).thenReturn(historicalOrder);
+
+        assertThrows(ServiceException.class, () -> fixture.service.userDetail(88L, 10L));
+
+        verify(fixture.userMapper, never()).selectOrdinaryUserIdsByNormalizedPhone(any());
+    }
+
+    @Test
+    void pickupRejectsMissingProofBeforeChangingState() {
+        Fixture fixture = new Fixture();
+        InfoUser rider = new InfoUser();
+        rider.setUserId(8L);
+        rider.setUserType("2");
+        when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
+        FlashDeliveryProofRequest request = new FlashDeliveryProofRequest();
+        request.setImageUrls(List.of());
+
+        assertThrows(RuntimeException.class, () -> fixture.service.pickup(8L, 10L, request));
+        verify(fixture.orderMapper, never()).transitionByRider(any(), any(), any(), any(), any(), any());
+    }
+
+    @Test
+    void atomicAcceptFailureDoesNotBindSecondRider() {
+        Fixture fixture = new Fixture();
+        InfoUser rider = new InfoUser();
+        rider.setUserId(8L);
+        rider.setUserType("2");
+        rider.setDeliveryType("FLASH");
+        when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
+        when(fixture.orderMapper.selectById(10L)).thenReturn(order(10L, "WAITING_ACCEPTANCE"));
+        when(fixture.orderMapper.accept(eq(10L), eq(8L), any())).thenReturn(0);
+
+        assertThrows(RuntimeException.class, () -> fixture.service.accept(8L, 10L));
+    }
+
+    // 预约单已过取件结束时间:即使骑手绕过列表直接持有订单 ID 请求接单,锁内校验也必须拒绝。
+    @Test
+    void expiredScheduledOrderCannotBeAcceptedEvenWhenItsIdIsKnown() {
+        Fixture fixture = new Fixture();
+        rider(fixture, 8L);
+        FlashDeliveryOrder order = order(10L, "WAITING_ACCEPTANCE");
+        Date now = new Date();
+        order.setDeliveryMode("SCHEDULED");
+        order.setScheduledPickupStartAt(new Date(now.getTime() - 31L * 60L * 1000L));
+        order.setScheduledPickupEndAt(new Date(now.getTime() - 60L * 1000L));
+        when(fixture.orderMapper.selectById(10L)).thenReturn(order);
+
+        assertThrows(ServiceException.class, () -> fixture.service.accept(8L, 10L));
+        verify(fixture.orderMapper, never()).accept(any(), any(), any());
+    }
+
+    @Test
+    void flashAcceptUsesRiderLockAndChecksAcceptancePolicyBeforeAtomicUpdate() {
+        Fixture fixture = new Fixture();
+        rider(fixture, 8L);
+        FlashDeliveryOrder waiting = order(10L, "WAITING_ACCEPTANCE");
+        FlashDeliveryOrder accepted = order(10L, ACCEPTED);
+        accepted.setRiderId(8L);
+        when(fixture.orderMapper.selectById(10L)).thenReturn(waiting, accepted);
+        when(fixture.orderMapper.accept(eq(10L), eq(8L), any(Date.class))).thenReturn(1);
+
+        fixture.service.accept(8L, 10L);
+
+        InOrder order = inOrder(fixture.lockService, fixture.acceptanceService, fixture.orderMapper);
+        order.verify(fixture.lockService).withLock(eq(8L), any(RiderDeliveryLockService.LockedCall.class));
+        order.verify(fixture.acceptanceService).assertCanAcceptFlash(8L, "NORMAL");
+        order.verify(fixture.orderMapper).accept(eq(10L), eq(8L), any(Date.class));
+    }
+
+    @Test
+    void flashAcceptanceConflictDoesNotUpdateOrder() {
+        Fixture fixture = new Fixture();
+        rider(fixture, 8L);
+        when(fixture.orderMapper.selectById(10L)).thenReturn(order(10L, "WAITING_ACCEPTANCE"));
+        doThrow(new ServiceException("exclusive"))
+                .when(fixture.acceptanceService).assertCanAcceptFlash(8L, "NORMAL");
+
+        assertThrows(ServiceException.class, () -> fixture.service.accept(8L, 10L));
+
+        verify(fixture.orderMapper, never()).accept(any(), any(), any());
+    }
+
+    @Test
+    void acceptSuccessNotifiesSenderOfOrder() {
+        Fixture fixture = new Fixture();
+        rider(fixture, 8L);
+        FlashDeliveryOrder waiting = order(10L, "WAITING_ACCEPTANCE");
+        FlashDeliveryOrder accepted = order(10L, ACCEPTED);
+        accepted.setRiderId(8L);
+        when(fixture.orderMapper.selectById(10L)).thenReturn(waiting, accepted);
+        when(fixture.orderMapper.accept(eq(10L), eq(8L), any(Date.class))).thenReturn(1);
+
+        fixture.service.accept(8L, 10L);
+
+        verify(fixture.notificationService).notifyAccepted(waiting);
+    }
+
+    @Test
+    void acceptFailureDoesNotTriggerNotification() {
+        Fixture fixture = new Fixture();
+        rider(fixture, 8L);
+        when(fixture.orderMapper.selectById(10L)).thenReturn(order(10L, "WAITING_ACCEPTANCE"));
+        when(fixture.orderMapper.accept(eq(10L), eq(8L), any(Date.class))).thenReturn(0);
+
+        assertThrows(ServiceException.class, () -> fixture.service.accept(8L, 10L));
+
+        verify(fixture.notificationService, never()).notifyAccepted(any());
+    }
+
+    @Test
+    void pickupSuccessNotifiesSenderOfOrder() {
+        Fixture fixture = new Fixture();
+        rider(fixture, 8L);
+        FlashDeliveryOrder accepted = order(10L, ACCEPTED);
+        accepted.setRiderId(8L);
+        when(fixture.orderMapper.selectById(10L)).thenReturn(accepted);
+        // 逐站取件走 updateFulfilment 条件更新而非 transitionByRider。
+        when(fixture.orderMapper.updateFulfilment(any())).thenReturn(1);
+        FlashDeliveryProofRequest request = new FlashDeliveryProofRequest();
+        request.setImageUrls(List.of("https://example.com/pickup.jpg"));
+
+        fixture.service.pickup(8L, 10L, request);
+
+        verify(fixture.notificationService).notifyPickedUp(accepted);
+    }
+
+    @Test
+    void deliverSuccessNotifiesSenderOfOrder() {
+        Fixture fixture = new Fixture();
+        rider(fixture, 8L);
+        FlashDeliveryOrder pickedUp = order(10L, "PICKED_UP");
+        pickedUp.setRiderId(8L);
+        when(fixture.orderMapper.selectById(10L)).thenReturn(pickedUp);
+        when(fixture.orderMapper.transitionByRider(eq(10L), eq(8L), eq("PICKED_UP"),
+                eq("DELIVERED"), eq("delivered_at"), any())).thenReturn(1);
+        FlashDeliveryDeliverRequest request = new FlashDeliveryDeliverRequest();
+        request.setPinCode("4821");
+        request.setImageUrls(List.of("https://example.com/delivery.jpg"));
+
+        fixture.service.deliver(8L, 10L, request);
+
+        verify(fixture.notificationService).notifyDelivered(pickedUp);
+    }
+
+    @Test
+    void proofTransitionFailureDoesNotTriggerNotification() {
+        Fixture fixture = new Fixture();
+        rider(fixture, 8L);
+        FlashDeliveryOrder accepted = order(10L, ACCEPTED);
+        accepted.setRiderId(8L);
+        when(fixture.orderMapper.selectById(10L)).thenReturn(accepted);
+        when(fixture.orderMapper.transitionByRider(eq(10L), eq(8L), eq("ACCEPTED"),
+                eq("PICKED_UP"), eq("picked_up_at"), any())).thenReturn(0);
+        FlashDeliveryProofRequest request = new FlashDeliveryProofRequest();
+        request.setImageUrls(List.of("https://example.com/pickup.jpg"));
+
+        assertThrows(ServiceException.class, () -> fixture.service.pickup(8L, 10L, request));
+
+        verify(fixture.notificationService, never()).notifyPickedUp(any());
+        verify(fixture.notificationService, never()).notifyDelivered(any());
+    }
+
+    @Test
+    void userCancelSuccessNotifiesAssignedRider() {
+        Fixture fixture = new Fixture();
+        FlashDeliveryOrder accepted = order(10L, ACCEPTED);
+        accepted.setRiderId(9L);
+        when(fixture.orderMapper.selectById(10L)).thenReturn(accepted);
+        when(fixture.orderMapper.cancel(eq(10L), eq("ACCEPTED"), eq("USER"), eq(7L),
+                anyString(), any(Date.class))).thenReturn(1);
+
+        fixture.service.userCancel(7L, 10L, reason("改行程"));
+
+        verify(fixture.notificationService).notifyCancelled(accepted);
+    }
+
+    @Test
+    void adminCancelSuccessNotifiesAssignedRider() {
+        Fixture fixture = new Fixture();
+        FlashDeliveryOrder pickedUp = order(10L, "PICKED_UP");
+        pickedUp.setRiderId(9L);
+        when(fixture.orderMapper.selectById(10L)).thenReturn(pickedUp);
+        // 平台异常取消先写介入审计,再走条件取消。
+        when(fixture.orderMapper.update(any(), any())).thenReturn(1);
+        when(fixture.orderMapper.cancel(eq(10L), eq("PICKED_UP"), eq("ADMIN"), eq(1L),
+                anyString(), any(Date.class))).thenReturn(1);
+        FlashDeliveryReasonRequest request = reason("异常收口");
+        request.setProblemStopId(101L);
+        request.setHandlingResult("平台介入收口");
+        // 已取货订单必须记录物品去向和交接凭证。
+        request.setPickedGoodsDisposition("已取物品交回寄件人");
+        request.setHandoffImageUrls(List.of("https://example.com/handoff.jpg"));
+
+        fixture.service.adminCancel(1L, 10L, request);
+
+        verify(fixture.notificationService).notifyCancelled(pickedUp);
+    }
+
+    @Test
+    void cancelFailureDoesNotTriggerNotification() {
+        Fixture fixture = new Fixture();
+        FlashDeliveryOrder accepted = order(10L, ACCEPTED);
+        accepted.setRiderId(9L);
+        when(fixture.orderMapper.selectById(10L)).thenReturn(accepted);
+        when(fixture.orderMapper.cancel(eq(10L), eq("ACCEPTED"), eq("USER"), eq(7L),
+                anyString(), any(Date.class))).thenReturn(0);
+
+        assertThrows(ServiceException.class, () -> fixture.service.userCancel(7L, 10L, reason("改行程")));
+
+        verify(fixture.notificationService, never()).notifyCancelled(any());
+    }
+
+    @Test
+    void excludedEventsNeverTriggerNotification() {
+        // 加小费、完成、过期预约自动取消三类事件不产生任何推送。
+        Fixture fixture = new Fixture();
+        editableOrder(fixture);
+        when(fixture.orderMapper.updateWaitingTip(any(), eq(2))).thenReturn(1);
+        fixture.service.addTip(7L, 10L, tipRequest(20L, 2));
+
+        // 合并后的 complete 要求订单已送达且全部取货站完成。
+        when(fixture.orderMapper.selectById(10L)).thenReturn(order(10L, "DELIVERED"));
+        when(fixture.orderMapper.transitionByStatus(eq(10L), eq("DELIVERED"), eq("COMPLETED"),
+                eq("completed_at"), any(Date.class))).thenReturn(1);
+        assertTrue(fixture.service.complete(1L, "ADMIN", 10L));
+
+        when(fixture.orderMapper.cancelExpiredScheduled(eq(10L), any(Date.class))).thenReturn(1);
+        assertTrue(fixture.service.cancelExpiredScheduledOrder(10L, new Date()));
+
+        verifyNoInteractions(fixture.notificationService);
+    }
+
+    @Test
+    void immediateOrderIsVisibleAndNotifiesAvailableRiders() {
+        Fixture fixture = new Fixture();
+        prepareCreate(fixture);
+        FlashDeliveryCreateRequest request = createRequest();
+        request.setDeliveryMode("NOW");
+        request.setScheduledPickupStartAt(null);
+        request.setScheduledPickupEndAt(null);
+
+        fixture.service.create(7L, request);
+
+        ArgumentCaptor<FlashDeliveryOrder> saved = ArgumentCaptor.forClass(FlashDeliveryOrder.class);
+        verify(fixture.orderMapper).insert(saved.capture());
+        assertTrue(saved.getValue().getIsDisplay());
+        verify(fixture.notificationService).notifyAvailable(saved.getValue());
+    }
+
+    @Test
+    void scheduledOrderStartsHiddenAndDoesNotNotifyBeforeWindow() {
+        Fixture fixture = new Fixture();
+        prepareCreate(fixture);
+
+        fixture.service.create(7L, createRequest());
+
+        ArgumentCaptor<FlashDeliveryOrder> saved = ArgumentCaptor.forClass(FlashDeliveryOrder.class);
+        verify(fixture.orderMapper).insert(saved.capture());
+        assertFalse(saved.getValue().getIsDisplay());
+        verify(fixture.notificationService, never()).notifyAvailable(any());
+    }
+
+    @Test
+    void scheduledOrderBecomesVisibleAndNotifiesOnlyAfterAtomicOpenSucceeds() {
+        Fixture fixture = new Fixture();
+        FlashDeliveryOrder scheduled = order(10L, "WAITING_ACCEPTANCE");
+        scheduled.setDeliveryMode("SCHEDULED");
+        scheduled.setIsDisplay(false);
+        Date now = new Date();
+        when(fixture.orderMapper.selectById(10L)).thenReturn(scheduled);
+        when(fixture.orderMapper.openScheduledForRiders(10L, now)).thenReturn(1);
+
+        assertTrue(fixture.service.openScheduledOrderForRiders(10L, now));
+
+        assertTrue(scheduled.getIsDisplay());
+        verify(fixture.notificationService).notifyAvailable(scheduled);
+
+        when(fixture.orderMapper.openScheduledForRiders(10L, now)).thenReturn(0);
+        assertFalse(fixture.service.openScheduledOrderForRiders(10L, now));
+        verify(fixture.notificationService).notifyAvailable(scheduled);
+    }
+
+    private FlashDeliveryReasonRequest reason(String text) {
+        FlashDeliveryReasonRequest request = new FlashDeliveryReasonRequest();
+        request.setReason(text);
+        return request;
+    }
+
+    @Test
+    void oversizedProofUrlIsRejectedBeforeStateTransition() {
+        Fixture fixture = new Fixture();
+        InfoUser rider = new InfoUser();
+        rider.setUserId(8L);
+        rider.setUserType("2");
+        when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
+        FlashDeliveryProofRequest request = new FlashDeliveryProofRequest();
+        request.setImageUrls(List.of("https://example.com/" + "a".repeat(1000)));
+
+        assertThrows(RuntimeException.class, () -> fixture.service.pickup(8L, 10L, request));
+        verify(fixture.orderMapper, never()).transitionByRider(any(), any(), any(), any(), any(), any());
+    }
+
+    @Test
+    void missingServiceTypeReturnsBusinessErrorInsteadOfNullPointer() {
+        Fixture fixture = new Fixture();
+        FlashDeliveryQuoteRequest request = request();
+        request.setServiceType(null);
+        assertThrows(ServiceException.class, () -> fixture.service.quote(request));
+    }
+
+    @Test
+    void equalTextAddressesAreRejectedEvenWhenCoordinatesDiffer() {
+        Fixture fixture = new Fixture();
+        FlashDeliveryQuoteRequest request = request();
+        request.getDelivery().setAddress(request.getPickup().getAddress());
+        request.getDelivery().setAddressDetail(request.getPickup().getAddressDetail());
+        assertThrows(ServiceException.class, () -> fixture.service.quote(request));
+    }
+
+    @Test
+    void concurrentPricingUpdateIsRejected() {
+        Fixture fixture = new Fixture();
+        when(fixture.pricingMapper.selectById(1L)).thenReturn(pricing());
+        when(fixture.pricingMapper.countOverlapping(eq("00:00"), eq("12:00"), anyInt(), eq(1L))).thenReturn(0);
+        when(fixture.pricingMapper.update(isNull(), any(Wrapper.class))).thenReturn(0);
+        FlashDeliveryPricingRequest request = pricingRequest();
+        request.setEndTime("12:00");
+        request.setConfigVersion(1);
+
+        assertThrows(ServiceException.class,
+                () -> fixture.service.updatePricing(1L, 1L, request));
+        verify(fixture.pricingMapper, never()).updateById(any(FlashDeliveryPricing.class));
+    }
+
+    @Test
+    void overlappingPricingPeriodIsRejectedBeforeInsert() {
+        Fixture fixture = new Fixture();
+        FlashDeliveryPricingRequest request = pricingRequest();
+        when(fixture.pricingMapper.countOverlapping(eq("00:00"), eq("24:00"), anyInt(), isNull())).thenReturn(1);
+
+        assertThrows(ServiceException.class, () -> fixture.service.createPricing(1L, request));
+
+        request.setDistance(new BigDecimal("0.004"));
+        assertThrows(ServiceException.class, () -> fixture.service.createPricing(1L, request));
+
+        verify(fixture.pricingMapper, never()).insert(any(FlashDeliveryPricing.class));
+    }
+
+    @Test
+    void pricingDistanceHalfCentimetreNormalizesToDatabaseMinimum() {
+        Fixture fixture = new Fixture();
+        FlashDeliveryPricingRequest request = pricingRequest();
+        request.setDistance(new BigDecimal("0.005"));
+
+        fixture.service.createPricing(1L, request);
+
+        ArgumentCaptor<FlashDeliveryPricing> saved = ArgumentCaptor.forClass(FlashDeliveryPricing.class);
+        verify(fixture.pricingMapper).insert(saved.capture());
+        assertEquals(new BigDecimal("0.01"), saved.getValue().getDistance());
+    }
+
+    @Test
+    void pricingDistanceThatRoundsToZeroIsRejectedBeforePersistence() {
+        Fixture fixture = new Fixture();
+        FlashDeliveryPricingRequest request = pricingRequest();
+        request.setDistance(new BigDecimal("0.001"));
+
+        assertThrows(ServiceException.class, () -> fixture.service.createPricing(1L, request));
+
+        verify(fixture.pricingMapper, never()).insert(any(FlashDeliveryPricing.class));
+    }
+
+    @Test
+    void createPricingPersistsImmediatelyActiveTimePeriod() {
+        Fixture fixture = new Fixture();
+        FlashDeliveryPricingRequest request = pricingRequest();
+        when(fixture.pricingMapper.countOverlapping(eq("00:00"), eq("24:00"), anyInt(), isNull())).thenReturn(0);
+
+        fixture.service.createPricing(7L, request);
+
+        ArgumentCaptor<FlashDeliveryPricing> saved = ArgumentCaptor.forClass(FlashDeliveryPricing.class);
+        verify(fixture.pricingMapper).insert(saved.capture());
+        assertEquals(1, saved.getValue().getConfigVersion());
+        assertEquals(7L, saved.getValue().getUpdatedBy());
+        assertEquals(90L, saved.getValue().getStartingFare());
+
+        InOrder writeOrder = inOrder(fixture.pricingMapper);
+        writeOrder.verify(fixture.pricingMapper)
+                .countOverlapping(eq("00:00"), eq("24:00"), anyInt(), isNull());
+        writeOrder.verify(fixture.pricingMapper).insert(any(FlashDeliveryPricing.class));
+    }
+
+    @Test
+    void homeOmitsServiceWithoutCurrentPricingPeriod() {
+        Fixture fixture = new Fixture();
+
+        assertTrue(fixture.service.home().getServices().isEmpty());
+    }
+
+    @Test
+    void quoteWithoutCurrentPricingPeriodFailsBeforeRouteLookup() {
+        Fixture fixture = new Fixture();
+
+        assertThrows(ServiceException.class, () -> fixture.service.quote(request()));
+
+        verify(fixture.routeService, never()).calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt());
+    }
+
+    @Test
+    void multipleCurrentPricingPeriodsFailExplicitlyBeforeRouteLookup() {
+        Fixture fixture = new Fixture();
+        when(fixture.pricingMapper.selectAtTime(anyString(), anyInt()))
+                .thenReturn(List.of(pricing(), pricing()));
+
+        assertThrows(ServiceException.class, () -> fixture.service.quote(request()));
+
+        verify(fixture.routeService, never()).calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt());
+    }
+
+    @Test
+    void updatingPricingChecksOverlapBeforeVersionedWrite() {
+        Fixture fixture = new Fixture();
+        FlashDeliveryPricing existing = pricing();
+        when(fixture.pricingMapper.selectById(1L)).thenReturn(existing);
+        when(fixture.pricingMapper.update(isNull(), any(Wrapper.class))).thenReturn(1);
+        FlashDeliveryPricingRequest request = pricingRequest();
+        request.setConfigVersion(1);
+
+        fixture.service.updatePricing(9L, 1L, request);
+
+        InOrder writeOrder = inOrder(fixture.pricingMapper);
+        writeOrder.verify(fixture.pricingMapper)
+                .countOverlapping(eq("00:00"), eq("24:00"), anyInt(), eq(1L));
+        writeOrder.verify(fixture.pricingMapper).update(isNull(), any(Wrapper.class));
+    }
+
+    @Test
+    void deletePricingRemovesConfigurationImmediately() {
+        Fixture fixture = new Fixture();
+        when(fixture.pricingMapper.selectById(18L)).thenReturn(pricing());
+        when(fixture.pricingMapper.deleteById(18L)).thenReturn(1);
+
+        fixture.service.deletePricing(18L);
+
+        verify(fixture.pricingMapper).deleteById(18L);
+    }
+
+    @Test
+    void urgentQuoteReturnsSeparatedFeesAndUsesScheduledPickupTime() {
+        Fixture fixture = new Fixture();
+        when(fixture.pricingMapper.selectAtTime(eq("16:30"), anyInt())).thenReturn(List.of(pricing()));
+        when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).thenReturn(new RouteDistance(6000, 900, "ROUTE"));
+        FlashDeliveryQuoteRequest request = request();
+        request.setDeliveryType("URGENT");
+        request.setTipAmount(5L);
+        request.setDeliveryMode("SCHEDULED");
+        Calendar calendar = Calendar.getInstance();
+        calendar.add(Calendar.DAY_OF_MONTH, 1);
+        calendar.set(Calendar.HOUR_OF_DAY, 16);
+        calendar.set(Calendar.MINUTE, 30);
+        calendar.set(Calendar.SECOND, 0);
+        calendar.set(Calendar.MILLISECOND, 0);
+        request.setScheduledPickupStartAt(calendar.getTime());
+        request.setScheduledPickupEndAt(new Date(calendar.getTimeInMillis() + 30L * 60L * 1000L));
+
+        var quote = fixture.service.quote(request);
+
+        assertEquals(45L, quote.getDistanceFee());
+        // 起步价与 45 元距离附加费分开返回,避免前端重复相加。
+        assertEquals(90L, quote.getBaseDeliveryFee());
+        assertEquals(27L, quote.getUrgentFee());
+        assertEquals(5L, quote.getTipAmount());
+        assertEquals(167L, quote.getAmount());
+        verify(fixture.pricingMapper).selectAtTime(eq("16:30"), anyInt());
+    }
+
+    @Test
+    void createRejectsChangedQuoteAndReturnsLatestQuoteWithoutInsert() {
+        Fixture fixture = new Fixture();
+        when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
+        when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).thenReturn(new RouteDistance(3200, 600, "ROUTE"));
+        FlashDeliveryCreateRequest request = createRequest();
+        request.setQuotedAmount(91L);
+
+        FlashDeliveryQuoteChangedException error = assertThrows(FlashDeliveryQuoteChangedException.class,
+                () -> fixture.service.create(7L, request));
+
+        assertEquals(90L, error.getLatestQuote().getAmount());
+        assertEquals(1L, error.getLatestQuote().getPricingId());
+        verify(fixture.orderMapper, never()).insert(any(FlashDeliveryOrder.class));
+    }
+
+    @Test
+    void createDefaultsPayTypeToCashAndStoresTransferSelection() {
+        Fixture fixture = new Fixture();
+        when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
+        when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).thenReturn(new RouteDistance(3200, 600, "ROUTE"));
+        doAnswer(invocation -> {
+            FlashDeliveryOrder order = invocation.getArgument(0);
+            order.setId(99L);
+            return 1;
+        }).when(fixture.orderMapper).insert(any(FlashDeliveryOrder.class));
+        when(fixture.imageMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
+        when(fixture.logMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
+
+        var defaultResult = fixture.service.create(7L, createRequest());
+        FlashDeliveryCreateRequest transfer = createRequest();
+        transfer.setPayType("6");
+        var transferResult = fixture.service.create(7L, transfer);
+
+        ArgumentCaptor<FlashDeliveryOrder> saved = ArgumentCaptor.forClass(FlashDeliveryOrder.class);
+        verify(fixture.orderMapper, times(2)).insert(saved.capture());
+        assertEquals("4", saved.getAllValues().get(0).getPayType());
+        assertEquals("6", saved.getAllValues().get(1).getPayType());
+        assertEquals("4", defaultResult.getPayType());
+        assertEquals("6", transferResult.getPayType());
+    }
+
+    @Test
+    void createRejectsUnsupportedPayType() {
+        Fixture fixture = new Fixture();
+        when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
+        when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).thenReturn(new RouteDistance(3200, 600, "ROUTE"));
+        FlashDeliveryCreateRequest request = createRequest();
+        request.setPayType("2");
+        org.mockito.Mockito.doThrow(new ServiceException("flash.delivery.pay.type.invalid"))
+                .when(fixture.paymentMethodGateService).assertUsable(eq("2"),
+                        eq(com.ruoyi.system.service.PaymentMethodGateService.GateScope.RIDER_FLASH), isNull(), isNull());
+
+        assertThrows(ServiceException.class, () -> fixture.service.create(7L, request));
+
+        verify(fixture.orderMapper, never()).insert(any(FlashDeliveryOrder.class));
+    }
+
+    @Test
+    void quoteValidatesDeliveryLevelAndCompleteItemInformation() {
+        Fixture fixture = new Fixture();
+        FlashDeliveryQuoteRequest request = request();
+        request.setDeliveryType("FAST");
+        FlashDeliveryQuoteRequest invalidDeliveryType = request;
+        assertThrows(ServiceException.class, () -> fixture.service.quote(invalidDeliveryType));
+
+        request = request();
+        request.setQuantity(0);
+        FlashDeliveryQuoteRequest invalidQuantity = request;
+        assertThrows(ServiceException.class, () -> fixture.service.quote(invalidQuantity));
+
+        request = request();
+        request.setWeightRange("SMALL");
+        FlashDeliveryQuoteRequest oldWeightRange = request;
+        assertThrows(ServiceException.class, () -> fixture.service.quote(oldWeightRange));
+    }
+
+    @Test
+    void quoteAcceptsAllFourWeightRanges() {
+        Fixture fixture = new Fixture();
+        when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
+        when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).thenReturn(new RouteDistance(3200, 600, "ROUTE"));
+
+        for (String weightRange : List.of("UP_TO_5_KG", "OVER_5_TO_10_KG",
+                "OVER_10_TO_15_KG", "OVER_15_TO_20_KG")) {
+            FlashDeliveryQuoteRequest request = request();
+            request.setWeightRange(weightRange);
+            var quote = assertDoesNotThrow(() -> fixture.service.quote(request), weightRange);
+            assertEquals(90L, quote.getAmount(), weightRange);
+        }
+    }
+
+    @Test
+    void createPersistsAndReturnsEveryWeightRange() {
+        for (String weightRange : List.of("UP_TO_5_KG", "OVER_5_TO_10_KG",
+                "OVER_10_TO_15_KG", "OVER_15_TO_20_KG")) {
+            Fixture fixture = new Fixture();
+            when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
+            when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).thenReturn(new RouteDistance(3200, 600, "ROUTE"));
+            doAnswer(invocation -> {
+                FlashDeliveryOrder order = invocation.getArgument(0);
+                order.setId(99L);
+                return 1;
+            }).when(fixture.orderMapper).insert(any(FlashDeliveryOrder.class));
+            when(fixture.imageMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
+            when(fixture.logMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
+            FlashDeliveryCreateRequest request = createRequest();
+            request.setWeightRange(weightRange);
+
+            var result = fixture.service.create(7L, request);
+
+            ArgumentCaptor<FlashDeliveryOrder> saved = ArgumentCaptor.forClass(FlashDeliveryOrder.class);
+            verify(fixture.orderMapper).insert(saved.capture());
+            assertEquals(weightRange, saved.getValue().getWeightRange());
+            assertEquals(weightRange, result.getWeightRange());
+        }
+    }
+
+    @Test
+    void quoteAllowsMissingWeightRange() {
+        Fixture fixture = new Fixture();
+        when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
+        when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt()))
+                .thenReturn(new RouteDistance(3200, 600, "ROUTE"));
+
+        FlashDeliveryQuoteRequest request = request();
+        request.setWeightRange(null);
+
+        assertDoesNotThrow(() -> fixture.service.quote(request));
+    }
+
+    @Test
+    void quoteRejectsUnknownWeightRangeWhenProvided() {
+        Fixture fixture = new Fixture();
+        for (String weightRange : new String[]{"", "SMALL", "OVER_20_KG"}) {
+            FlashDeliveryQuoteRequest request = request();
+            request.setWeightRange(weightRange);
+            assertThrows(ServiceException.class, () -> fixture.service.quote(request), String.valueOf(weightRange));
+        }
+    }
+
+    private FlashDeliveryQuoteRequest request() {
+        FlashDeliveryQuoteRequest request = new FlashDeliveryQuoteRequest();
+        request.setServiceType("HELP_SEND");
+        request.setDeliveryType("NORMAL");
+        request.setVehicleType(1);
+        request.setPackageType("DOCUMENT");
+        request.setQuantity(1);
+        request.setWeightRange("UP_TO_5_KG");
+        request.setSpecification("30 x 20 x 10 cm");
+        request.setTipAmount(0L);
+        request.setDeliveryMode("NOW");
+        request.setPickup(address("A", "0911111111", "25.0330", "121.5645"));
+        request.setDelivery(address("B", "0922222222", "25.0478", "121.5319"));
+        request.getDelivery().setAddress("second road");
+        return request;
+    }
+
+    private FlashDeliveryCreateRequest createRequest() {
+        FlashDeliveryCreateRequest request = new FlashDeliveryCreateRequest();
+        FlashDeliveryQuoteRequest quote = request();
+        request.setServiceType(quote.getServiceType());
+        request.setDeliveryType(quote.getDeliveryType());
+        request.setVehicleType(quote.getVehicleType());
+        request.setPackageType(quote.getPackageType());
+        request.setQuantity(quote.getQuantity());
+        request.setWeightRange(quote.getWeightRange());
+        request.setSpecification(quote.getSpecification());
+        request.setTipAmount(quote.getTipAmount());
+        request.setPickup(quote.getPickup());
+        request.setDelivery(quote.getDelivery());
+        request.setClientRequestId("flash-scheduled-001");
+        request.setDeliveryMode("SCHEDULED");
+        Date start = new Date(System.currentTimeMillis() + 60L * 60L * 1000L);
+        request.setScheduledPickupStartAt(start);
+        request.setScheduledPickupEndAt(new Date(start.getTime() + 30L * 60L * 1000L));
+        request.setPinRequired(true);
+        request.setPricingId(1L);
+        request.setPricingVersion(1);
+        request.setQuotedBaseDeliveryFee(90L);
+        request.setQuotedDistanceFee(0L);
+        request.setQuotedUrgentFee(0L);
+        request.setQuotedAmount(90L);
+        request.setSenderImageUrls(List.of("https://example.com/sender.jpg"));
+        return request;
+    }
+
+    private void prepareCreate(Fixture fixture) {
+        when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
+        when(fixture.routeService.calculate(any(), any(), anyInt()))
+                .thenReturn(new RouteDistance(3200, 600, "ROUTE"));
+        when(fixture.userMapper.selectOrdinaryUserIdsByNormalizedPhone("0922222222")).thenReturn(List.of());
+        doAnswer(invocation -> {
+            FlashDeliveryOrder order = invocation.getArgument(0);
+            order.setId(99L);
+            return 1;
+        }).when(fixture.orderMapper).insert(any(FlashDeliveryOrder.class));
+    }
+
+    private FlashDeliveryAddressRequest address(String name, String phone, String lat, String lon) {
+        FlashDeliveryAddressRequest address = new FlashDeliveryAddressRequest();
+        address.setName(name);
+        address.setPhone(phone);
+        address.setAddress("road");
+        address.setAddressDetail("door");
+        address.setLatitude(new BigDecimal(lat));
+        address.setLongitude(new BigDecimal(lon));
+        return address;
+    }
+
+    private FlashDeliveryPricing pricing() {
+        FlashDeliveryPricing pricing = new FlashDeliveryPricing();
+        pricing.setId(1L);
+        pricing.setStartTime("00:00");
+        pricing.setEndTime("24:00");
+        pricing.setStartingDistance(new BigDecimal("3.00"));
+        pricing.setStartingFare(90L);
+        pricing.setDistance(new BigDecimal("1.00"));
+        pricing.setFreight(15L);
+        pricing.setUrgentRate(new BigDecimal("20.00"));
+        pricing.setMinimumUrgentFee(10L);
+        pricing.setConfigVersion(1);
+        return pricing;
+    }
+
+    private FlashDeliveryPricingRequest pricingRequest() {
+        FlashDeliveryPricingRequest request = new FlashDeliveryPricingRequest();
+        request.setVehicleType(1);
+        request.setStartTime("00:00");
+        request.setEndTime("24:00");
+        request.setStartingDistance(new BigDecimal("3.00"));
+        request.setStartingFare(90L);
+        request.setDistance(new BigDecimal("1.00"));
+        request.setFreight(15L);
+        request.setUrgentRate(new BigDecimal("20.00"));
+        request.setMinimumUrgentFee(10L);
+        return request;
+    }
+
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    private void assertRiderTab(String tab, List<String> statuses) {
+        Fixture fixture = new Fixture();
+        rider(fixture, 8L);
+        when(fixture.orderMapper.selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
+                any(Wrapper.class))).thenReturn(new Page<FlashDeliveryOrder>());
+
+        fixture.service.riderOrders(8L, 1, 10, tab, null, null);
+
+        ArgumentCaptor<com.baomidou.mybatisplus.core.conditions.query.QueryWrapper> query =
+                ArgumentCaptor.forClass(com.baomidou.mybatisplus.core.conditions.query.QueryWrapper.class);
+        verify(fixture.orderMapper).selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class), query.capture());
+        // MyBatis-Plus 的条件参数在渲染 SQL 段时才写入参数表,必须先取 SQL 再断言参数。
+        String sql = query.getValue().getSqlSegment();
+        assertTrue(sql.contains("rider_id"), tab);
+        var parameters = query.getValue().getParamNameValuePairs().values();
+        assertTrue(parameters.contains(8L), tab);
+        for (String status : statuses) assertTrue(parameters.contains(status), tab + ":" + status);
+    }
+
+    private void rider(Fixture fixture, Long riderId) {
+        InfoUser rider = new InfoUser();
+        rider.setUserId(riderId);
+        rider.setUserType("2");
+        rider.setDeliveryType("FLASH");
+        when(fixture.userMapper.selectInfoUserByUserId(riderId)).thenReturn(rider);
+    }
+
+    private com.ruoyi.app.flashdelivery.dto.FlashDeliveryRatingRequest rating(Double stars) {
+        com.ruoyi.app.flashdelivery.dto.FlashDeliveryRatingRequest request =
+                new com.ruoyi.app.flashdelivery.dto.FlashDeliveryRatingRequest();
+        request.setStars(stars);
+        return request;
+    }
+
+    private FlashDeliveryOrder order(Long id, String status) {
+        FlashDeliveryOrder order = new FlashDeliveryOrder();
+        order.setId(id);
+        order.setPickupStopCount(1); order.setPickedUpStopCount(0); order.setVersion(0);
+        order.setOrderNo("FL-20260813-021");
+        order.setUserId(7L);
+        order.setServiceType("HELP_SEND");
+        order.setDeliveryType("NORMAL");
+        order.setStatus(status);
+        order.setIsDisplay(true);
+        order.setPackageType("DOCUMENT");
+        order.setQuantity(1);
+        order.setWeightRange("UP_TO_5_KG");
+        order.setDeliveryMode("NOW");
+        order.setPinRequired(true);
+        order.setDeliveryPinCode("4821");
+        order.setPickupName("pickup contact");
+        order.setPickupPhone("0911111111");
+        order.setPickupAddress("pickup road");
+        order.setPickupAddressDetail("pickup door");
+        order.setPickupCity("台北市");
+        order.setPickupArea("中山區");
+        order.setPickupLongitude(new BigDecimal("121.5100"));
+        order.setPickupLatitude(new BigDecimal("25.0100"));
+        order.setDeliveryName("delivery contact");
+        order.setDeliveryPhone("0922222222");
+        order.setDeliveryAddress("delivery road");
+        order.setDeliveryAddressDetail("delivery door");
+        order.setDeliveryCity("台北市");
+        order.setDeliveryArea("大安區");
+        order.setDeliveryLongitude(new BigDecimal("121.5200"));
+        order.setDeliveryLatitude(new BigDecimal("25.0200"));
+        order.setDistanceMeters(3200);
+        order.setEstimatedDurationSeconds(2100);
+        order.setDistanceFee(12L);
+        order.setBaseDeliveryFee(102L);
+        order.setUrgentRate(new BigDecimal("20.00"));
+        order.setMinimumUrgentFee(10L);
+        order.setUrgentFee(0L);
+        order.setTipAmount(10L);
+        order.setAmount(112L);
+        order.setCurrency("TWD");
+        order.setUserNote("private note");
+        order.setCreateTime(new Date());
+        return order;
+    }
+
+    private FlashDeliveryOrderImage image(String proofType, String url) {
+        FlashDeliveryOrderImage image = new FlashDeliveryOrderImage();
+        image.setProofType(proofType);
+        image.setImageUrl(url);
+        return image;
+    }
+
+
+    @Test
+    void missingOrInvalidBodyOrderIdIsRejectedBeforeDatabaseAccess() {
+        Fixture fixture = new Fixture();
+        for (Long orderId : java.util.Arrays.asList(null, 0L, -1L)) {
+            ServiceException addressFailure = assertThrows(ServiceException.class,
+                    () -> fixture.service.updateAddress(7L, orderId, new FlashDeliveryAddressConfirmRequest()));
+            ServiceException tipFailure = assertThrows(ServiceException.class,
+                    () -> fixture.service.addTip(7L, orderId, tipRequest(20L, 2)));
+            assertEquals("flash.delivery.order.not.found", addressFailure.getMessage());
+            assertEquals("flash.delivery.order.not.found", tipFailure.getMessage());
+        }
+        org.mockito.Mockito.verifyNoInteractions(fixture.orderMapper);
+    }
+
+    @Test
+    void addressQuoteUsesOriginalPricingSnapshotAndDoesNotWrite() {
+        Fixture fixture = new Fixture();
+        FlashDeliveryOrder order = editableOrder(fixture);
+        // FR-074/093:预约单也保留原运价快照,平台调价不得改变旧单的计价参数。
+        order.setDeliveryMode("SCHEDULED");
+        order.setScheduledPickupStartAt(new Date(1));
+        order.setScheduledPickupEndAt(new Date(1800001));
+        FlashDeliveryQuoteView quote = fixture.service.quoteAddress(7L, 10L, addressChange());
+        assertEquals(4500, quote.getDistanceMeters());
+        assertEquals(900, quote.getEstimatedDurationSeconds());
+        assertEquals(23L, quote.getDistanceFee());
+        // 基础配送费只表示起步价,距离附加费单独返回。
+        assertEquals(90L, quote.getBaseDeliveryFee());
+        assertEquals(10L, quote.getTipAmount());
+        assertEquals(123L, quote.getAmount());
+        assertEquals(2, quote.getOrderVersion());
+        assertEquals(1, quote.getPricingVersion());
+        assertEquals("pickup road", order.getPickupAddress());
+        // 地址变化只重算路线,不查询最新运价。
+        verify(fixture.pricingMapper, never()).selectAtTime(anyString(), anyInt());
+        verify(fixture.orderMapper, never()).updateWaitingAddress(any(), any());
+        verify(fixture.logMapper, never()).insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderLog.class));
+    }
+
+    @Test
+    void addressSaveUpdatesBothEndsRouteFeesAndReceiverTogether() {
+        Fixture fixture = new Fixture();
+        FlashDeliveryOrder order = editableOrder(fixture);
+        when(fixture.userMapper.selectOrdinaryUserIdsByNormalizedPhone("0922222222"))
+                .thenReturn(List.of(88L));
+        when(fixture.orderMapper.updateWaitingAddress(any(), eq(2))).thenReturn(1);
+        FlashDeliveryAddressConfirmRequest input = confirmAddress(fixture);
+        input.getPickup().setName(" new sender ");
+        input.getPickup().setAddressDetail(null);
+        input.getDelivery().setName("new receiver");
+        var view = fixture.service.updateAddress(7L, 10L, input);
+        assertEquals("new sender", view.getPickup().getName());
+        assertEquals("new receiver", view.getDelivery().getName());
+        assertNull(view.getPickup().getAddressDetail());
+        assertEquals(4500, view.getDistanceMeters());
+        assertEquals(123L, view.getAmount());
+        assertEquals(3, view.getOrderVersion());
+        assertEquals(88L, order.getReceiverUserId());
+        assertEquals(10L, order.getTipAmount());
+        assertEquals("WAITING_ACCEPTANCE", order.getStatus());
+        verify(fixture.orderMapper).updateWaitingAddress(order, 2);
+        verify(fixture.logMapper).insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderLog.class));
+    }
+
+    @Test
+    void addressChangeClearsFormerReceiverWhenNewPhoneDoesNotMatch() {
+        Fixture fixture = new Fixture();
+        FlashDeliveryOrder order = editableOrder(fixture);
+        order.setReceiverUserId(88L);
+        when(fixture.orderMapper.updateWaitingAddress(any(), eq(2))).thenReturn(1);
+        fixture.service.updateAddress(7L, 10L, confirmAddress(fixture));
+        assertNull(order.getReceiverUserId());
+    }
+
+    @Test
+    void addressQuotePreservesUrgentRulesAndExistingTip() {
+        Fixture fixture = new Fixture();
+        FlashDeliveryOrder order = editableOrder(fixture);
+        order.setDeliveryType("URGENT");
+        var quote = fixture.service.quoteAddress(7L, 10L, addressChange());
+        assertEquals(23L, quote.getUrgentFee());
+        assertEquals(146L, quote.getAmount());
+        assertEquals(10L, quote.getTipAmount());
+    }
+
+    @Test
+    void changedRouteOrUnconfirmedPriceReturnsLatestQuoteWithoutSaving() {
+        Fixture fixture = new Fixture();
+        editableOrder(fixture);
+        FlashDeliveryAddressConfirmRequest input = confirmAddress(fixture);
+        input.setQuotedAmount(1L);
+        var exception = assertThrows(FlashDeliveryQuoteChangedException.class,
+                () -> fixture.service.updateAddress(7L, 10L, input));
+        assertEquals(123L, exception.getLatestQuote().getAmount());
+        input.setQuotedAmount(123L);
+        input.setQuotedDistanceMeters(4499);
+        assertThrows(FlashDeliveryQuoteChangedException.class,
+                () -> fixture.service.updateAddress(7L, 10L, input));
+        input.setQuotedDistanceMeters(4500);
+        input.setQuotedUrgentFee(null);
+        assertThrows(FlashDeliveryQuoteChangedException.class,
+                () -> fixture.service.updateAddress(7L, 10L, input));
+        verify(fixture.orderMapper, never()).updateWaitingAddress(any(), any());
+        verify(fixture.logMapper, never()).insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderLog.class));
+    }
+
+    @Test
+    void receiverAndOtherUsersCannotQuoteEditOrAddTip() {
+        Fixture fixture = new Fixture();
+        FlashDeliveryOrder order = editableOrder(fixture);
+        order.setReceiverUserId(88L);
+        for (Long user : List.of(88L, 99L)) {
+            assertThrows(ServiceException.class, () -> fixture.service.quoteAddress(user, 10L, addressChange()));
+            assertThrows(ServiceException.class, () -> fixture.service.updateAddress(user, 10L, new FlashDeliveryAddressConfirmRequest()));
+            assertThrows(ServiceException.class, () -> fixture.service.addTip(user, 10L, tipRequest(5L, 2)));
+        }
+        verify(fixture.routeService, never()).calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt());
+        verify(fixture.orderMapper, never()).updateWaitingTip(any(), any());
+    }
+
+    @Test
+    void everyNonWaitingStateAndAssignedRiderRejectsBothChanges() {
+        for (String status : List.of("ACCEPTED", "PICKED_UP", "DELIVERED", "COMPLETED", "CANCELLED")) {
+            Fixture fixture = new Fixture();
+            FlashDeliveryOrder order = editableOrder(fixture);
+            order.setStatus(status);
+            assertThrows(ServiceException.class, () -> fixture.service.quoteAddress(7L, 10L, addressChange()));
+            assertThrows(ServiceException.class, () -> fixture.service.updateAddress(7L, 10L, new FlashDeliveryAddressConfirmRequest()));
+            assertThrows(ServiceException.class, () -> fixture.service.addTip(7L, 10L, tipRequest(5L, 2)));
+        }
+        Fixture fixture = new Fixture();
+        editableOrder(fixture).setRiderId(8L);
+        assertThrows(ServiceException.class, () -> fixture.service.quoteAddress(7L, 10L, addressChange()));
+        assertThrows(ServiceException.class, () -> fixture.service.addTip(7L, 10L, tipRequest(5L, 2)));
+    }
+
+    @Test
+    void staleOrMissingVersionCannotOverwriteNewerOrder() {
+        Fixture fixture = new Fixture();
+        editableOrder(fixture);
+        FlashDeliveryAddressChangeRequest input = addressChange();
+        input.setOrderVersion(1);
+        assertThrows(ServiceException.class, () -> fixture.service.quoteAddress(7L, 10L, input));
+        for (Integer version : java.util.Arrays.asList(null, -1, 1)) {
+            assertThrows(ServiceException.class,
+                    () -> fixture.service.addTip(7L, 10L, tipRequest(5L, version)));
+        }
+        verify(fixture.orderMapper, never()).updateWaitingTip(any(), any());
+    }
+
+    @Test
+    void addressValidationRejectsSameInvalidAndOverDistanceEndpoints() {
+        Fixture fixture = new Fixture();
+        editableOrder(fixture);
+        FlashDeliveryAddressChangeRequest input = addressChange();
+        input.setDelivery(input.getPickup());
+        assertThrows(ServiceException.class, () -> fixture.service.quoteAddress(7L, 10L, input));
+        input.setDelivery(request().getDelivery());
+        input.getPickup().setPhone("");
+        assertThrows(ServiceException.class, () -> fixture.service.quoteAddress(7L, 10L, input));
+        when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).thenReturn(new RouteDistance(40001, 1000, "GOOGLE"));
+        assertThrows(ServiceException.class, () -> fixture.service.quoteAddress(7L, 10L, addressChange()));
+        verify(fixture.orderMapper, never()).updateWaitingAddress(any(), any());
+    }
+
+    @Test
+    void riderAcceptanceBetweenReadAndAddressSaveRejectsWriteWithoutLog() {
+        Fixture fixture = new Fixture();
+        editableOrder(fixture);
+        FlashDeliveryAddressConfirmRequest input = confirmAddress(fixture);
+        when(fixture.orderMapper.updateWaitingAddress(any(), eq(2))).thenReturn(0);
+        ServiceException exception = assertThrows(ServiceException.class,
+                () -> fixture.service.updateAddress(7L, 10L, input));
+        assertEquals("flash.delivery.state.changed", exception.getMessage());
+        verify(fixture.logMapper, never()).insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderLog.class));
+    }
+
+    @Test
+    void addingTipOnlyIncreasesTipAndTotalAndRejectsSameVersionRetry() {
+        Fixture fixture = new Fixture();
+        FlashDeliveryOrder order = editableOrder(fixture);
+        when(fixture.orderMapper.updateWaitingTip(any(), eq(2))).thenReturn(1);
+        var view = fixture.service.addTip(7L, 10L, tipRequest(20L, 2));
+        assertEquals(30L, view.getTipAmount());
+        assertEquals(132L, view.getAmount());
+        assertEquals(102L, view.getBaseDeliveryFee());
+        assertEquals(3200, view.getDistanceMeters());
+        assertEquals(3, view.getOrderVersion());
+        assertThrows(ServiceException.class, () -> fixture.service.addTip(7L, 10L, tipRequest(20L, 2)));
+        assertEquals(30L, order.getTipAmount());
+        verify(fixture.orderMapper).updateWaitingTip(any(), eq(2));
+        verify(fixture.routeService, never()).calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt());
+        verify(fixture.pricingMapper, never()).selectAtTime(anyString(), anyInt());
+    }
+
+    @Test
+    void tipChangeInvalidatesEarlierAddressConfirmation() {
+        Fixture fixture = new Fixture();
+        editableOrder(fixture);
+        FlashDeliveryAddressConfirmRequest confirmation = confirmAddress(fixture);
+        when(fixture.orderMapper.updateWaitingTip(any(), eq(2))).thenReturn(1);
+        fixture.service.addTip(7L, 10L, tipRequest(20L, 2));
+        assertThrows(ServiceException.class, () -> fixture.service.updateAddress(7L, 10L, confirmation));
+        verify(fixture.orderMapper, never()).updateWaitingAddress(any(), any());
+    }
+
+    @Test
+    void additionalTipRejectsFractionalJsonBeforeWriting() throws Exception {
+        Fixture fixture = new Fixture();
+        editableOrder(fixture);
+        when(fixture.orderMapper.updateWaitingTip(any(), eq(2))).thenReturn(1);
+        FlashDeliveryTipAddRequest input = new com.fasterxml.jackson.databind.ObjectMapper().readValue(
+                "{\"orderVersion\":2,\"additionalTipAmount\":1.9}", FlashDeliveryTipAddRequest.class);
+        ServiceException exception = assertThrows(ServiceException.class,
+                () -> fixture.service.addTip(7L, 10L, input));
+        assertEquals("flash.delivery.tip.additional.invalid", exception.getMessage());
+        verify(fixture.orderMapper, never()).updateWaitingTip(any(), any());
+    }
+
+    @Test
+    void additionalTipRejectsJsonAmountBeyondLongRange() throws Exception {
+        Fixture fixture = new Fixture();
+        editableOrder(fixture);
+        FlashDeliveryTipAddRequest input = new com.fasterxml.jackson.databind.ObjectMapper().readValue(
+                "{\"orderVersion\":2,\"additionalTipAmount\":1e100}", FlashDeliveryTipAddRequest.class);
+        ServiceException exception = assertThrows(ServiceException.class,
+                () -> fixture.service.addTip(7L, 10L, input));
+        assertEquals("flash.delivery.tip.additional.invalid", exception.getMessage());
+        verify(fixture.orderMapper, never()).updateWaitingTip(any(), any());
+    }
+
+    @Test
+    void additionalTipRejectsZeroNegativeNullAndOverflow() {
+        Fixture fixture = new Fixture();
+        editableOrder(fixture);
+        for (Long value : java.util.Arrays.asList(null, 0L, -1L, Long.MAX_VALUE)) {
+            assertThrows(ServiceException.class,
+                    () -> fixture.service.addTip(7L, 10L, tipRequest(value, 2)));
+        }
+        verify(fixture.orderMapper, never()).updateWaitingTip(any(), any());
+    }
+
+    @Test
+    void riderAcceptanceBetweenReadAndTipSaveRejectsWriteWithoutLog() {
+        Fixture fixture = new Fixture();
+        editableOrder(fixture);
+        when(fixture.orderMapper.updateWaitingTip(any(), eq(2))).thenReturn(0);
+        assertThrows(ServiceException.class, () -> fixture.service.addTip(7L, 10L, tipRequest(20L, 2)));
+        verify(fixture.logMapper, never()).insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderLog.class));
+    }
+
+    private FlashDeliveryOrder editableOrder(Fixture fixture) {
+        FlashDeliveryOrder order = order(10L, "WAITING_ACCEPTANCE");
+        order.setRiderId(null);
+        order.setVersion(2);
+        order.setPricingId(1L);
+        order.setPricingVersion(1);
+        order.setPricingStartTime("00:00");
+        order.setPricingEndTime("24:00");
+        order.setStartingDistance(new BigDecimal("3.00"));
+        order.setStartingFare(90L);
+        order.setDistance(new BigDecimal("1.00"));
+        order.setFreight(15L);
+        when(fixture.orderMapper.selectById(10L)).thenReturn(order);
+        when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).thenReturn(new RouteDistance(4500, 900, "GOOGLE"));
+        // 后台现价与快照不同;改址仍以订单保存的价格计算。
+        when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
+        return order;
+    }
+
+    private FlashDeliveryAddressChangeRequest addressChange() {
+        FlashDeliveryAddressChangeRequest input = new FlashDeliveryAddressChangeRequest();
+        input.setOrderVersion(2);
+        input.setPickup(request().getPickup());
+        input.setDelivery(request().getDelivery());
+        return input;
+    }
+
+    private FlashDeliveryAddressConfirmRequest confirmAddress(Fixture fixture) {
+        FlashDeliveryAddressChangeRequest change = addressChange();
+        FlashDeliveryQuoteView quote = fixture.service.quoteAddress(7L, 10L, change);
+        FlashDeliveryAddressConfirmRequest input = new FlashDeliveryAddressConfirmRequest();
+        input.setOrderVersion(quote.getOrderVersion());
+        input.setPickup(change.getPickup());
+        input.setDelivery(change.getDelivery());
+        input.setQuotedDistanceMeters(quote.getDistanceMeters());
+        input.setQuotedBaseDeliveryFee(quote.getBaseDeliveryFee());
+        input.setQuotedDistanceFee(quote.getDistanceFee());
+        input.setQuotedUrgentFee(quote.getUrgentFee());
+        input.setQuotedAmount(quote.getAmount());
+        return input;
+    }
+
+    private FlashDeliveryTipAddRequest tipRequest(Long amount, Integer version) {
+        FlashDeliveryTipAddRequest input = new FlashDeliveryTipAddRequest();
+        input.setAdditionalTipAmount(amount == null ? null : BigDecimal.valueOf(amount));
+        input.setOrderVersion(version);
+        return input;
+    }
+
+    @Test
+    void generateOrderNoUsesTimestampAndSequenceFormat() {
+        String first = FlashDeliveryApplicationService.generateOrderNo();
+        String second = FlashDeliveryApplicationService.generateOrderNo();
+
+        assertTrue(isFlashOrderNo(first), "应为 98 前缀 17 位纯数字,实际: " + first);
+        assertTrue(isFlashOrderNo(second), "应为 98 前缀 17 位纯数字,实际: " + second);
+        assertNotEquals(first, second, "同毫秒连续生成应由序列位区分,不得同号");
+    }
+
+    @Test
+    void createRegeneratesOrderNoWhenUniqueKeyConflicts() {
+        Fixture fixture = new Fixture();
+        when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
+        when(fixture.routeService.calculate(any(), any(), anyInt())).thenReturn(new RouteDistance(3200, 600, "ROUTE"));
+        when(fixture.userMapper.selectOrdinaryUserIdsByNormalizedPhone(anyString())).thenReturn(List.of());
+        org.springframework.dao.DataIntegrityViolationException orderNoConflict =
+                new org.springframework.dao.DataIntegrityViolationException("insert failed",
+                        new java.sql.SQLException(
+                                "Duplicate entry '98001' for key 'flash_delivery_order.uk_flash_order_no'"));
+        // 捕获器拿到的是同一对象引用,重试改单号后两次取值相同;改为每次调用时记录当时单号
+        java.util.List<String> attempted = new java.util.ArrayList<>();
+        doAnswer(invocation -> {
+            FlashDeliveryOrder order = invocation.getArgument(0);
+            attempted.add(order.getOrderNo());
+            if (attempted.size() == 1) throw orderNoConflict;
+            order.setId(99L);
+            return 1;
+        }).when(fixture.orderMapper).insert(any(FlashDeliveryOrder.class));
+
+        fixture.service.create(7L, createRequest());
+
+        verify(fixture.orderMapper, times(2)).insert(any(FlashDeliveryOrder.class));
+        assertEquals(2, attempted.size());
+        assertNotEquals(attempted.get(0), attempted.get(1), "撞号重试后应换新单号");
+        assertTrue(isFlashOrderNo(attempted.get(1)), "重试后仍应为新格式单号");
+    }
+
+    @Test
+    void createKeepsIdempotencyFallbackForNonOrderNoConflicts() {
+        Fixture fixture = new Fixture();
+        when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
+        when(fixture.routeService.calculate(any(), any(), anyInt())).thenReturn(new RouteDistance(3200, 600, "ROUTE"));
+        when(fixture.userMapper.selectOrdinaryUserIdsByNormalizedPhone(anyString())).thenReturn(List.of());
+        org.springframework.dao.DataIntegrityViolationException idempotentConflict =
+                new org.springframework.dao.DataIntegrityViolationException("insert failed",
+                        new java.sql.SQLException(
+                                "Duplicate entry '7-req-1' for key 'flash_delivery_order.uk_user_request'"));
+        doThrow(idempotentConflict).when(fixture.orderMapper).insert(any(FlashDeliveryOrder.class));
+        // 前置幂等查询放行(null);插入冲突后 catch 内按 v1 实现走 FOR UPDATE 复查 + lockOrder 锁单
+        FlashDeliveryOrder concurrent = new FlashDeliveryOrder();
+        concurrent.setId(55L);
+        concurrent.setOrderNo("981789000000011");
+        when(fixture.orderMapper.selectByUserRequestId(eq(7L), anyString())).thenReturn(null);
+        when(fixture.orderMapper.selectOne(any(Wrapper.class))).thenReturn(concurrent);
+        when(fixture.orderMapper.selectById(55L)).thenReturn(concurrent);
+
+        var detail = fixture.service.create(7L, createRequest());
+
+        assertEquals(55L, detail.getId());
+        verify(fixture.orderMapper, times(1)).insert(any(FlashDeliveryOrder.class));
+    }
+
+    /** 闪送新格式单号:98 开头、17 位纯数字。 */
+    private static boolean isFlashOrderNo(String orderNo) {
+        return orderNo != null && orderNo.startsWith("98") && orderNo.length() == 17
+                && orderNo.chars().allMatch(Character::isDigit);
+    }
+
+    private static class Fixture {
+        final com.ruoyi.system.mapper.flash.FlashDeliveryOrderStopMapper stopMapper = mock(com.ruoyi.system.mapper.flash.FlashDeliveryOrderStopMapper.class);
+        final com.ruoyi.system.mapper.flash.FlashDeliveryTargetAdjustmentMapper adjustmentMapper = mock(com.ruoyi.system.mapper.flash.FlashDeliveryTargetAdjustmentMapper.class);
+        final FlashDeliveryOrderMapper orderMapper = mock(FlashDeliveryOrderMapper.class);
+        final FlashDeliveryPricingMapper pricingMapper = mock(FlashDeliveryPricingMapper.class);
+        final FlashDeliveryOrderImageMapper imageMapper = mock(FlashDeliveryOrderImageMapper.class);
+        final FlashDeliveryOrderLogMapper logMapper = mock(FlashDeliveryOrderLogMapper.class);
+        final InfoUserMapper userMapper = mock(InfoUserMapper.class);
+        final com.ruoyi.system.mapper.InfoBankCardMapper bankCardMapper = mock(com.ruoyi.system.mapper.InfoBankCardMapper.class);
+        final FlashDeliveryRouteService routeService = mock(FlashDeliveryRouteService.class);
+        final RiderDeliveryLockService lockService = mock(RiderDeliveryLockService.class);
+        final RiderDeliveryAcceptanceService acceptanceService = mock(RiderDeliveryAcceptanceService.class);
+        final com.ruoyi.system.service.PaymentMethodGateService paymentMethodGateService =
+                mock(com.ruoyi.system.service.PaymentMethodGateService.class);
+        final FlashDeliveryNotificationService notificationService = mock(FlashDeliveryNotificationService.class);
+        final FlashDeliveryApplicationService service = new FlashDeliveryApplicationService(
+                orderMapper, pricingMapper, imageMapper, logMapper, userMapper, bankCardMapper,
+                routeService, new FlashDeliveryPricingCalculator(), lockService, acceptanceService, stopMapper, adjustmentMapper,
+                paymentMethodGateService, notificationService);
+
+        Fixture() {
+            when(paymentMethodGateService.listAvailable(any(), any(), any())).thenReturn(List.of(
+                    new com.ruoyi.system.service.PaymentMethodGateService.PayMethodOption("COD", "1", true),
+                    new com.ruoyi.system.service.PaymentMethodGateService.PayMethodOption("LINE_PAY", "3", true),
+                    new com.ruoyi.system.service.PaymentMethodGateService.PayMethodOption("OFFLINE_TRANSFER", "6", true)));
+            // Existing single-stop scenarios now explicitly provide station rows; production never synthesizes legacy stops.
+            when(orderMapper.selectByIdForUpdate(any())).thenAnswer(i -> {
+                FlashDeliveryOrder current = orderMapper.selectById((Long) i.getArgument(0));
+                if (current != null && current.getPickupStopCount() == null) current.setPickupStopCount(1);
+                return current;
+            });
+            when(stopMapper.insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderStop.class))).thenReturn(1);
+            when(stopMapper.updateById(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderStop.class))).thenReturn(1);
+            when(imageMapper.insert(any(FlashDeliveryOrderImage.class))).thenReturn(1);
+            when(stopMapper.selectList(any())).thenAnswer(i -> {
+                FlashDeliveryOrder current = orderMapper.selectById(10L);
+                var pickup = new com.ruoyi.system.domain.flash.FlashDeliveryOrderStop();
+                pickup.setId(101L); pickup.setOrderId(10L); pickup.setStopType("PICKUP"); pickup.setStopOrder(0);
+                pickup.setName("pickup contact"); pickup.setPhone("0911111111"); pickup.setAddress("pickup road");
+                pickup.setLatitude(new BigDecimal("25.0100")); pickup.setLongitude(new BigDecimal("121.5100"));
+                pickup.setPackageType("DOCUMENT"); pickup.setQuantity(1); pickup.setWeightRange("UP_TO_5_KG");
+                pickup.setStatus(current != null && List.of("PICKED_UP", "DELIVERED", "COMPLETED").contains(current.getStatus())
+                        ? "PICKED_UP" : "PENDING");
+                var delivery = new com.ruoyi.system.domain.flash.FlashDeliveryOrderStop();
+                delivery.setId(102L); delivery.setOrderId(10L); delivery.setStopType("DELIVERY"); delivery.setStopOrder(1); delivery.setStatus("PENDING");
+                return List.of(pickup, delivery);
+            });
+            when(lockService.withLock(any(), any())).thenAnswer(invocation -> {
+                RiderDeliveryLockService.LockedCall<?> call = invocation.getArgument(1);
+                return call.call();
+            });
+        }
+    }
+}

+ 1 - 1
ruoyi-admin/src/test/java/com/ruoyi/app/flashdelivery/service/FlashDeliveryMultiPickupTest.java

@@ -413,7 +413,7 @@ class FlashDeliveryMultiPickupTest {
         final FlashDeliveryPricingMapper prices = mock(FlashDeliveryPricingMapper.class);
         final InfoUserMapper users = mock(InfoUserMapper.class);
         final FlashDeliveryRouteService routes = mock(FlashDeliveryRouteService.class);
-        final FlashDeliveryApplicationService service = new FlashDeliveryApplicationService(orders, prices, images, logs, users,
+        final FlashDeliveryApplicationService service = new FlashDeliveryApplicationService(orders, prices, images, logs, users, mock(com.ruoyi.system.mapper.InfoBankCardMapper.class),
                 routes, new FlashDeliveryPricingCalculator(), mock(RiderDeliveryLockService.class), mock(RiderDeliveryAcceptanceService.class),
                 stops, adjustments, mock(com.ruoyi.system.service.PaymentMethodGateService.class),
                 mock(FlashDeliveryNotificationService.class));

+ 8 - 4
ruoyi-system/src/main/java/com/ruoyi/system/service/PaymentMethodGateService.java

@@ -152,8 +152,8 @@ public class PaymentMethodGateService {
 
     /**
      * 列出某维度对收款方当前可用的支付方式(结算页可选项、骑手端可选集共用)。
-     * 返回平台开关 ∩ 收款方选择的全部项并附就绪度;RIDER_FLASH 维度就绪度恒 true
-     * (收款方未定或即骑手本人,线下卡是否就绪由骑手自选方式时自行把握,抢单过滤兜底)。
+     * 返回平台开关 ∩ 收款方选择的全部项并附就绪度;RIDER_FLASH 维度仅线下转账在骑手身份
+     * 已知时按启用卡判定就绪度,其余及骑手未定(用户下单)恒 true。
      */
     public List<PayMethodOption> listAvailable(GateScope scope, Long storeId, Long payeeUserId) {
         return listAvailable(scope, storeId, payeeUserId, null);
@@ -223,11 +223,15 @@ public class PaymentMethodGateService {
 
     /**
      * 就绪度判定:MERCHANT 维度线上渠道看门店级凭证、线下转账看商家启用卡;
-     * RIDER_FLASH 维度恒满足(收款方未定,由骑手选择层与抢单过滤兜底)。
+     * RIDER_FLASH 维度仅线下转账在骑手身份已知时要求启用卡。
      */
     private boolean isReady(String group, GateScope scope, Long storeId, Long payeeUserId) {
         if (group.equals(GROUP_COD) || group.equals(GROUP_CASH)) return true;
-        if (scope == GateScope.RIDER_FLASH) return true;
+        if (scope == GateScope.RIDER_FLASH) {
+            // 线下转账:骑手身份已知(抢单池/骑手端可选集)要求启用卡;用户下单骑手未定(null)放行
+            return !GROUP_OFFLINE.equals(group) || payeeUserId == null || bankCardMapper.selectCount(
+                    new QueryWrapper<InfoBankCard>().eq("user_id", payeeUserId).eq("is_active", 1)) > 0;
+        }
         switch (group) {
             case GROUP_CARD_OMG:
                 return storeId != null && omgMapper.selectCount(

+ 19 - 0
ruoyi-system/src/test/java/com/ruoyi/system/service/PaymentMethodGateServiceTest.java

@@ -240,6 +240,25 @@ class PaymentMethodGateServiceTest {
                 service.assertUsableForOrder("4", PaymentMethodGateService.GateScope.MERCHANT, 1L, 7L, null));
     }
 
+    @Test
+    void riderOfflineReadinessRequiresActiveCardWhenRiderKnown() {
+        // 收款方=骑手本人(抢单池/骑手端可选集):无启用卡时线下转账不可用,有卡恢复
+        when(bankCardMapper.selectCount(any())).thenReturn(0L);
+        assertThrows(ServiceException.class, () ->
+                service.assertUsable("6", PaymentMethodGateService.GateScope.RIDER_FLASH, null, 8L));
+        assertTrue(service.listAvailable(PaymentMethodGateService.GateScope.RIDER_FLASH, null, 8L).stream()
+                .filter(o -> "6".equals(o.getPayType())).allMatch(o -> !o.isReady()));
+        when(bankCardMapper.selectCount(any())).thenReturn(1L);
+        assertDoesNotThrow(() ->
+                service.assertUsable("6", PaymentMethodGateService.GateScope.RIDER_FLASH, null, 8L));
+        assertTrue(service.listAvailable(PaymentMethodGateService.GateScope.RIDER_FLASH, null, 8L).stream()
+                .filter(o -> "6".equals(o.getPayType())).allMatch(PaymentMethodGateService.PayMethodOption::isReady));
+        // 用户下单(骑手未定 payee=null):不受卡就绪度限制,保持可选
+        when(bankCardMapper.selectCount(any())).thenReturn(0L);
+        assertTrue(service.listAvailable(PaymentMethodGateService.GateScope.RIDER_FLASH, null, null).stream()
+                .filter(o -> "6".equals(o.getPayType())).allMatch(PaymentMethodGateService.PayMethodOption::isReady));
+    }
+
     @Test
     void cashGroupOnlyInMerchantScopeDomains() {
         // 值域:商家勾选与平台开关矩阵均可选 CASH;骑手闪送维度不含 CASH