Ver código fonte

feat(骑手端): 新增美食与闪送订单页签角标计数接口

- GET /system/orderQsOprate/orderCount 一次返回美食订单 newTask/toPickup/delivering 计数,抽取 orderList 页签条件共用保证口径一致
- GET /system/flashDelivery/rider/orderCount 复用 riderOrdersQuery 同口径返回闪送三页签计数
- 未开通对应配送类型时 newTask 计 0;补契约与定向测试(131 项通过),spec 006/024 顺延登记
qmj 5 dias atrás
pai
commit
95459ba65f

+ 16 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/flashdelivery/controller/FlashDeliveryRiderController.java

@@ -42,6 +42,22 @@ public class FlashDeliveryRiderController extends BaseController {
         return success(service.riderOrders(userId(token), page, size, tab, longitude, latitude));
     }
 
+    /**
+     * 骑手端任务角标数量:一次返回闪送订单 newTask/toPickup/delivering 三个页签的计数,
+     * App 无需逐页签切换请求;newTask 距离过滤口径与列表一致。
+     *
+     * @param token 登录令牌,用于解析当前骑手 ID
+     * @param longitude 骑手当前经度;选填,传入时必须与纬度同时提供
+     * @param latitude 骑手当前纬度;选填,传入时必须与经度同时提供
+     */
+    @GetMapping("/orderCount")
+    @Auth
+    public AjaxResult orderCount(@RequestHeader String token,
+                                 @RequestParam(required = false) BigDecimal longitude,
+                                 @RequestParam(required = false) BigDecimal latitude) {
+        return success(service.riderOrderCounts(userId(token), longitude, latitude));
+    }
+
     /** 接单前隐藏敏感字段;接单后仅中单骑手可见完整履约信息。 */
     @GetMapping("/orders/{id}")
     @Auth

+ 14 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/flashdelivery/dto/FlashDeliveryRiderOrderCountView.java

@@ -0,0 +1,14 @@
+package com.ruoyi.app.flashdelivery.dto;
+
+import lombok.Data;
+
+/** 骑手端任务角标数量:一次返回三个页签的计数,App 无需逐页签切换请求。 */
+@Data
+public class FlashDeliveryRiderOrderCountView {
+    /** 新任务(可抢任务)数量;骑手未开通闪送时为 0。 */
+    private long newTask;
+    /** 待取件数量。 */
+    private long toPickup;
+    /** 配送中数量。 */
+    private long delivering;
+}

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

@@ -552,6 +552,30 @@ public class FlashDeliveryApplicationService {
         return result;
     }
 
+    /**
+     * 骑手端任务角标数量:一次返回闪送订单 newTask/toPickup/delivering 三个页签的计数,
+     * App 无需逐页签切换请求。newTask 与列表同口径(车型、支付方式、可抢时间窗与可选距离上限);
+     * 骑手未开通闪送时 newTask 计 0(列表接口保持原有报错行为)。
+     */
+    public FlashDeliveryRiderOrderCountView riderOrderCounts(Long riderId, BigDecimal longitude, BigDecimal latitude) {
+        InfoUser rider = requireRider(riderId);
+        Set<String> acceptedPayTypes = acceptedRiderPayTypes(rider);
+        Integer riderVehicleType = riderVehicleType(rider);
+        FlashDeliveryRiderOrderCountView view = new FlashDeliveryRiderOrderCountView();
+        view.setToPickup(orderMapper.selectCount(
+                riderOrdersQuery(riderId, riderVehicleType, "toPickup", null, null, false, acceptedPayTypes)));
+        view.setDelivering(orderMapper.selectCount(
+                riderOrdersQuery(riderId, riderVehicleType, "delivering", null, null, false, acceptedPayTypes)));
+        if (rider.supportsDeliveryType("FLASH")) {
+            validateRiderCoordinates(longitude, latitude);
+            view.setNewTask(orderMapper.selectCount(
+                    riderOrdersQuery(riderId, riderVehicleType, "newTask", longitude, latitude, false, acceptedPayTypes)));
+        } else {
+            view.setNewTask(0L);
+        }
+        return view;
+    }
+
     /** 骑手任务详情:抢单前返回脱敏视图(隐藏联系人、电话、坐标和 PIN);接单后仅中单骑手可见完整信息。 */
     public FlashDeliveryOrderDetailView riderDetail(Long riderId, Long orderId) {
         return riderDetail(riderId, requireOrder(orderId));

+ 100 - 44
ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderQsOprateController.java

@@ -361,50 +361,7 @@ public class PosOrderQsOprateController extends BaseController {
             }
         }
 
-        LambdaQueryWrapper<PosOrder> wrapper = new LambdaQueryWrapper<>();
-        wrapper.eq(PosOrder::getIsDisplay,true);
-        switch (tab) {
-            case "newTask":
-                wrapper.eq(PosOrder::getType, 0L).eq(PosOrder::getDeliveryStatus, 0L)
-                        .eq(PosOrder::getState, 0L).eq(PosOrder::getPayStatus, 1L)
-                        .eq(PosOrder::getAfterSaleStatus, 0L).isNull(PosOrder::getQsId)
-                        .eq(PosOrder::getSelfDelivery, 0);
-                if (longitude != null && latitude != null) {
-                    // 距离上限(公里)从字典取,未配置则不过滤
-                    Integer limitKm = getNewTaskDistanceLimit();
-                    if (limitKm != null) {
-                        long limitMeters = limitKm.longValue() * 1000L;
-                        wrapper.apply("ST_Distance_Sphere(point(longitude, latitude), point({0}, {1})) <= {2}",
-                                longitude, latitude, limitMeters);
-                    }
-                    wrapper.last("ORDER BY ST_Distance_Sphere(point(longitude, latitude), point(" + longitude + ", " + latitude + ")) ASC");
-                } else {
-                    wrapper.orderByDesc(PosOrder::getCretim);
-                }
-                break;
-            case "toPickup":
-                wrapper.eq(PosOrder::getDeliveryStatus, 1L).eq(PosOrder::getQsId, riderId)
-                        .in(PosOrder::getState, 0L, 1L, 2L)
-                        .eq(PosOrder::getAfterSaleStatus, 0L).orderByAsc(PosOrder::getCretim);
-                break;
-            case "delivering":
-                wrapper.eq(PosOrder::getDeliveryStatus, 2L).eq(PosOrder::getQsId, riderId)
-                        .eq(PosOrder::getState, 2L).eq(PosOrder::getAfterSaleStatus, 0L)
-                        .orderByAsc(PosOrder::getCretim);
-                break;
-                //已送达
-            case "completed":
-                wrapper.eq(PosOrder::getDeliveryStatus, 3L).eq(PosOrder::getAfterSaleStatus, 0L).eq(PosOrder::getQsId, riderId).orderByDesc(PosOrder::getCretim);
-                break;
-            case "cancelled":
-                wrapper.eq(PosOrder::getState, 4L).eq(PosOrder::getQsId, riderId).eq(PosOrder::getAfterSaleStatus, 0L).orderByDesc(PosOrder::getCretim);
-                break;
-            case "refund":
-                wrapper.gt(PosOrder::getAfterSaleStatus, 0L).eq(PosOrder::getQsId, riderId).orderByDesc(PosOrder::getCretim);
-                break;
-            default:
-                break;
-        }
+        LambdaQueryWrapper<PosOrder> wrapper = orderTabWrapper(tab, riderId, longitude, latitude, true);
 
         com.baomidou.mybatisplus.extension.plugins.pagination.Page<PosOrder> pageParam = new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(page, size);
         com.baomidou.mybatisplus.extension.plugins.pagination.Page<PosOrder> result = posOrderService.page(pageParam, wrapper);
@@ -448,6 +405,105 @@ public class PosOrderQsOprateController extends BaseController {
         return success(org);
     }
 
+    /**
+     * 骑手端订单角标数量:一次返回美食订单 newTask/toPickup/delivering 三个页签的订单数,
+     * App 无需逐页签切换请求。newTask 过滤条件(含可选距离上限)与 orderList 完全一致;
+     * 骑手未开通美食配送时 newTask 计 0(列表接口保持原有报错行为)。
+     */
+    @Anonymous
+    @Auth
+    @GetMapping("/orderCount")
+    public AjaxResult orderCount(@RequestHeader String token,
+                                 @RequestParam(required = false) BigDecimal longitude,
+                                 @RequestParam(required = false) BigDecimal latitude) {
+        JwtUtil jwtUtil = new JwtUtil();
+        String qsId = jwtUtil.getusid(token);
+        Long riderId = Long.valueOf(qsId);
+        InfoUser rider = requireRider(qsId);
+
+        long newTaskCount = 0L;
+        if (rider.supportsDeliveryType("FOOD")) {
+            newTaskCount = posOrderService.count(orderTabWrapper("newTask", riderId, longitude, latitude, false));
+        }
+        long toPickupCount = posOrderService.count(orderTabWrapper("toPickup", riderId, null, null, false));
+        long deliveringCount = posOrderService.count(orderTabWrapper("delivering", riderId, null, null, false));
+
+        JSONObject org = new JSONObject();
+        org.put("newTask", newTaskCount);
+        org.put("toPickup", toPickupCount);
+        org.put("delivering", deliveringCount);
+        return success(org);
+    }
+
+    /**
+     * 按页签构建骑手订单查询条件;orderList 分页与 orderCount 计数共用,保证角标数量与列表口径一致。
+     * withOrder 为 false 时跳过排序子句,仅用于计数。
+     */
+    private LambdaQueryWrapper<PosOrder> orderTabWrapper(String tab, Long riderId, BigDecimal longitude,
+                                                         BigDecimal latitude, boolean withOrder) {
+        LambdaQueryWrapper<PosOrder> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(PosOrder::getIsDisplay,true);
+        switch (tab) {
+            case "newTask":
+                wrapper.eq(PosOrder::getType, 0L).eq(PosOrder::getDeliveryStatus, 0L)
+                        .eq(PosOrder::getState, 0L).eq(PosOrder::getPayStatus, 1L)
+                        .eq(PosOrder::getAfterSaleStatus, 0L).isNull(PosOrder::getQsId)
+                        .eq(PosOrder::getSelfDelivery, 0);
+                if (longitude != null && latitude != null) {
+                    // 距离上限(公里)从字典取,未配置则不过滤
+                    Integer limitKm = getNewTaskDistanceLimit();
+                    if (limitKm != null) {
+                        long limitMeters = limitKm.longValue() * 1000L;
+                        wrapper.apply("ST_Distance_Sphere(point(longitude, latitude), point({0}, {1})) <= {2}",
+                                longitude, latitude, limitMeters);
+                    }
+                    if (withOrder) {
+                        wrapper.last("ORDER BY ST_Distance_Sphere(point(longitude, latitude), point(" + longitude + ", " + latitude + ")) ASC");
+                    }
+                } else if (withOrder) {
+                    wrapper.orderByDesc(PosOrder::getCretim);
+                }
+                break;
+            case "toPickup":
+                wrapper.eq(PosOrder::getDeliveryStatus, 1L).eq(PosOrder::getQsId, riderId)
+                        .in(PosOrder::getState, 0L, 1L, 2L)
+                        .eq(PosOrder::getAfterSaleStatus, 0L);
+                if (withOrder) {
+                    wrapper.orderByAsc(PosOrder::getCretim);
+                }
+                break;
+            case "delivering":
+                wrapper.eq(PosOrder::getDeliveryStatus, 2L).eq(PosOrder::getQsId, riderId)
+                        .eq(PosOrder::getState, 2L).eq(PosOrder::getAfterSaleStatus, 0L);
+                if (withOrder) {
+                    wrapper.orderByAsc(PosOrder::getCretim);
+                }
+                break;
+                //已送达
+            case "completed":
+                wrapper.eq(PosOrder::getDeliveryStatus, 3L).eq(PosOrder::getAfterSaleStatus, 0L).eq(PosOrder::getQsId, riderId);
+                if (withOrder) {
+                    wrapper.orderByDesc(PosOrder::getCretim);
+                }
+                break;
+            case "cancelled":
+                wrapper.eq(PosOrder::getState, 4L).eq(PosOrder::getQsId, riderId).eq(PosOrder::getAfterSaleStatus, 0L);
+                if (withOrder) {
+                    wrapper.orderByDesc(PosOrder::getCretim);
+                }
+                break;
+            case "refund":
+                wrapper.gt(PosOrder::getAfterSaleStatus, 0L).eq(PosOrder::getQsId, riderId);
+                if (withOrder) {
+                    wrapper.orderByDesc(PosOrder::getCretim);
+                }
+                break;
+            default:
+                break;
+        }
+        return wrapper;
+    }
+
     /**
      * 返回订单详情,带上store、user信息(返回格式与 orderList 列表项一致,仅返回单条)
      * @param token

+ 20 - 0
ruoyi-admin/src/test/java/com/ruoyi/app/flashdelivery/controller/FlashDeliveryControllerContractTest.java

@@ -138,6 +138,26 @@ class FlashDeliveryControllerContractTest {
                         String.class, int.class, int.class, String.class, String.class));
     }
 
+    @Test
+    void riderOrderCountEndpointUsesOptionalCoordinatesAndReturnsCounts() throws Exception {
+        FlashDeliveryApplicationService service = mock(FlashDeliveryApplicationService.class);
+        var counts = new com.ruoyi.app.flashdelivery.dto.FlashDeliveryRiderOrderCountView();
+        when(service.riderOrderCounts(8L, BigDecimal.ONE, BigDecimal.TEN)).thenReturn(counts);
+
+        AjaxResult result = new FlashDeliveryRiderController(service)
+                .orderCount(JwtUtil.token("8", "rider"), BigDecimal.ONE, BigDecimal.TEN);
+
+        assertSame(counts, result.get(AjaxResult.DATA_TAG));
+
+        Method orderCount = FlashDeliveryRiderController.class.getMethod("orderCount",
+                String.class, BigDecimal.class, BigDecimal.class);
+        assertArrayEquals(new String[]{"/orderCount"}, orderCount.getAnnotation(GetMapping.class).value());
+        assertNotNull(orderCount.getParameters()[0].getAnnotation(RequestHeader.class));
+        for (int index = 1; index < orderCount.getParameterCount(); index++) {
+            assertTrue(!orderCount.getParameters()[index].getAnnotation(RequestParam.class).required());
+        }
+    }
+
     @Test
     void appDetailAndAcceptPutDirectOrderDtoInAjaxData() {
         FlashDeliveryApplicationService service = mock(FlashDeliveryApplicationService.class);

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

@@ -1739,6 +1739,57 @@ class FlashDeliveryApplicationServiceTest {
         for (String status : statuses) assertTrue(parameters.contains(status), tab + ":" + status);
     }
 
+    @Test
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    void riderOrderCountsReturnsBadgeCountsForAllListTabs() {
+        Fixture fixture = new Fixture();
+        rider(fixture, 8L);
+        when(fixture.orderMapper.selectCount(any(Wrapper.class))).thenReturn(3L);
+
+        com.ruoyi.app.flashdelivery.dto.FlashDeliveryRiderOrderCountView view =
+                fixture.service.riderOrderCounts(8L, null, null);
+
+        assertEquals(3L, view.getNewTask());
+        assertEquals(3L, view.getToPickup());
+        assertEquals(3L, view.getDelivering());
+        ArgumentCaptor<com.baomidou.mybatisplus.core.conditions.query.QueryWrapper> query =
+                ArgumentCaptor.forClass(com.baomidou.mybatisplus.core.conditions.query.QueryWrapper.class);
+        verify(fixture.orderMapper, times(3)).selectCount(query.capture());
+        // 角标与列表页签同口径:三个页签各查一次(toPickup 已接单、delivering 已取件、newTask 待抢单)
+        List<Object> allParams = new java.util.ArrayList<>();
+        boolean[] hasPendingGrab = {false};
+        for (com.baomidou.mybatisplus.core.conditions.query.QueryWrapper captured : query.getAllValues()) {
+            String sql = captured.getSqlSegment().toLowerCase();
+            allParams.addAll(captured.getParamNameValuePairs().values());
+            assertFalse(sql.contains("order by"));
+            if (sql.contains("rider_id is null")) hasPendingGrab[0] = true;
+        }
+        assertTrue(hasPendingGrab[0]);
+        assertTrue(allParams.contains("ACCEPTED"));
+        assertTrue(allParams.contains("PICKED_UP"));
+        assertTrue(allParams.contains("WAITING_ACCEPTANCE"));
+    }
+
+    @Test
+    void riderOrderCountsReportsZeroNewTaskWhenFlashNotEnabled() {
+        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.selectCount(any(Wrapper.class))).thenReturn(4L);
+
+        com.ruoyi.app.flashdelivery.dto.FlashDeliveryRiderOrderCountView view =
+                fixture.service.riderOrderCounts(8L, null, null);
+
+        // 未开通闪送无可抢任务,newTask 角标计 0,仅统计本人取件中/配送中
+        assertEquals(0L, view.getNewTask());
+        assertEquals(4L, view.getToPickup());
+        assertEquals(4L, view.getDelivering());
+        verify(fixture.orderMapper, times(2)).selectCount(any(Wrapper.class));
+    }
+
     private void rider(Fixture fixture, Long riderId) {
         InfoUser rider = new InfoUser();
         rider.setUserId(riderId);

+ 65 - 0
ruoyi-admin/src/test/java/com/ruoyi/app/order/PosOrderQsOprateControllerTest.java

@@ -5,7 +5,9 @@ import com.baomidou.mybatisplus.core.conditions.Wrapper;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.alibaba.fastjson.JSONObject;
 import com.ruoyi.app.service.UserService;
+import com.ruoyi.common.core.domain.AjaxResult;
 import com.ruoyi.common.exception.ServiceException;
 import com.ruoyi.system.domain.InfoUser;
 import com.ruoyi.system.domain.PosOrder;
@@ -26,7 +28,9 @@ import org.springframework.test.util.ReflectionTestUtils;
 
 import java.math.BigDecimal;
 import java.util.Collections;
+import java.util.List;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -35,6 +39,7 @@ import static org.mockito.ArgumentMatchers.anyString;
 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;
 
@@ -153,4 +158,64 @@ class PosOrderQsOprateControllerTest {
                 () -> controller.deliverOrder(JwtUtil.token("88", "rider"), input));
         verify(orders, never()).saveOrUpdate(any(PosOrder.class));
     }
+
+    @Test
+    void orderCountReturnsBadgeCountsUsingTheSameTabFiltersAsOrderList() {
+        PosOrderQsOprateController controller = new PosOrderQsOprateController();
+        IPosOrderService orders = mock(IPosOrderService.class);
+        UserService userService = mock(UserService.class);
+        ReflectionTestUtils.setField(controller, "posOrderService", orders);
+        ReflectionTestUtils.setField(controller, "userService", userService);
+        InfoUser rider = new InfoUser();
+        rider.setUserId(88L);
+        rider.setUserType("2");
+        rider.setDeliveryType("FOOD");
+        when(userService.checkUserStatus(88L)).thenReturn(rider);
+        when(orders.count(any())).thenReturn(3L);
+
+        AjaxResult result = controller.orderCount(JwtUtil.token("88", "rider"), null, null);
+
+        JSONObject data = (JSONObject) result.get(AjaxResult.DATA_TAG);
+        assertEquals(3L, (long) data.getLong("newTask"));
+        assertEquals(3L, (long) data.getLong("toPickup"));
+        assertEquals(3L, (long) data.getLong("delivering"));
+        ArgumentCaptor<LambdaQueryWrapper<PosOrder>> wrapperCaptor = ArgumentCaptor.forClass(LambdaQueryWrapper.class);
+        verify(orders, times(3)).count(wrapperCaptor.capture());
+        List<LambdaQueryWrapper<PosOrder>> wrappers = wrapperCaptor.getAllValues();
+        // 角标与列表页签同口径:newTask 限定待抢单,取件中/配送中限定本人订单
+        assertTrue(wrappers.get(0).getSqlSegment().toLowerCase().contains("qs_id is null"));
+        assertTrue(wrappers.get(0).getSqlSegment().toLowerCase().contains("self_delivery"));
+        assertTrue(wrappers.get(1).getSqlSegment().toLowerCase().contains("qs_id"));
+        assertTrue(wrappers.get(1).getParamNameValuePairs().containsValue(1L));
+        assertTrue(wrappers.get(2).getSqlSegment().toLowerCase().contains("qs_id"));
+        assertTrue(wrappers.get(2).getParamNameValuePairs().containsValue(2L));
+        // 计数查询不附加排序子句
+        for (LambdaQueryWrapper<PosOrder> wrapper : wrappers) {
+            assertFalse(wrapper.getSqlSegment().toLowerCase().contains("order by"));
+        }
+    }
+
+    @Test
+    void orderCountReportsZeroNewTaskWhenFoodDeliveryNotEnabled() {
+        PosOrderQsOprateController controller = new PosOrderQsOprateController();
+        IPosOrderService orders = mock(IPosOrderService.class);
+        UserService userService = mock(UserService.class);
+        ReflectionTestUtils.setField(controller, "posOrderService", orders);
+        ReflectionTestUtils.setField(controller, "userService", userService);
+        InfoUser rider = new InfoUser();
+        rider.setUserId(88L);
+        rider.setUserType("2");
+        rider.setDeliveryType("FLASH");
+        when(userService.checkUserStatus(88L)).thenReturn(rider);
+        when(orders.count(any())).thenReturn(5L);
+
+        AjaxResult result = controller.orderCount(JwtUtil.token("88", "rider"), null, null);
+
+        JSONObject data = (JSONObject) result.get(AjaxResult.DATA_TAG);
+        // 未开通美食配送无可抢任务,newTask 角标计 0,仅统计本人取件中/配送中
+        assertEquals(0L, (long) data.getLong("newTask"));
+        assertEquals(5L, (long) data.getLong("toPickup"));
+        assertEquals(5L, (long) data.getLong("delivering"));
+        verify(orders, times(2)).count(any());
+    }
 }

+ 5 - 0
specs/006-orderstate/tasks.md

@@ -247,3 +247,8 @@ Task T022: "骑手端 PosOrderQsOprateController.orderList()"
 - [x] T042 为商家全部页签、空页签及未传页签补充接单过滤回归,修改前 8 个新增用例均因缺少条件失败。
 - [x] T043 在 `orderList` 分页前统一限制外送订单须 `deliveryStatus IN (1,2,3)` 且 `qsId` 非空,保留自取/堂食、支付和门店权限条件。
 - [x] T044 使用 JDK 21 运行 `OrderListPaymentFilterTest`、`PosOrderShOprateControllerTest`、`PosOrderQsOprateControllerTest` 定向测试,23/23 通过;未执行真实数据库分页联调和完整回归。
+
+## 2026-10-14 增量:骑手端美食订单角标计数
+
+- [x] T045 新增 `GET /system/orderQsOprate/orderCount`,一次返回美食订单 newTask/toPickup/delivering 三个页签计数;抽取 orderList 页签查询条件共用(计数跳过排序子句),newTask 过滤口径与列表一致(含可选距离上限与美食配送类型门禁,未开通计 0)。
+- [x] T046 补充 PosOrderQsOprateControllerTest 角标计数定向测试;JDK 21 定向测试 131 项通过。App 联调未执行。

+ 5 - 0
specs/024-flash-delivery/tasks.md

@@ -242,3 +242,8 @@
 - [x] T121 联动 019 接入文档及最终全量/构建证据,数据库迁移与真实资金/多连接竞态另行验证。未提交/推送。
 
 Phase 24 本地验证:最终后端 164 类/1140 项测试、0 失败/错误/跳过,JDK 21 package 成功;平台 16/16、build:stage 成功。独立审查 5 项重要问题均回归修复,取件资金采用当前读、退款终态防降级、恢复按到期时间公平排队。详见 019/tasks.md;SQL、真实数据库竞态、真实支付退款及 App 联调未执行。
+
+## Phase 25:骑手端闪送角标计数(2026-10-14)
+
+- [x] T122 新增 `GET /system/flashDelivery/rider/orderCount`,一次返回闪送订单 newTask/toPickup/delivering 三个页签计数;复用 riderOrdersQuery 同口径查询(车型、支付方式、时间窗与可选距离上限),未开通闪送的骑手 newTask 计 0。
+- [x] T123 新增 FlashDeliveryRiderOrderCountView 与契约/服务定向测试;JDK 21 定向测试通过。App 联调未执行。