# 闪送“我收的”与急送独占 Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 为闪送订单增加创建时收件账号绑定和“我收的”查询,并用骑手级 Redisson 分布式锁保证 `URGENT` 跨外卖、闪送独占。 **Architecture:** `flash_delivery_order.receiver_user_id` 保存创建时唯一匹配的普通用户,不动态追溯;用户列表通过 `role` 在数据库分页前切换寄件人/收件人条件。`RiderDeliveryLockService` 统一串行化同一骑手的接单请求,`RiderDeliveryExclusivityService` 统一查询两类进行中订单;闪送和外卖接单都在同一锁键内完成检查与原子更新。 **Tech Stack:** Java 21、Spring Boot、Spring Transaction、MyBatis/MyBatis-Plus、MySQL、Redisson、JUnit 5、Mockito。 ## Global Constraints - 不接入闪送支付、退款、结算或骑手收入分账;创建成功直接进入 `WAITING_ACCEPTANCE`。 - 所有闪送金额保持整数 TWD。 - 数据库迁移只能追加到 `updatesql/sql.md`,不得直接执行。 - Controller token 使用 `@RequestHeader String token`,查询参数显式使用 `@RequestParam`。 - 业务错误必须使用五语言 i18n key,不硬编码单一语言。 - 保持 `ruoyi-admin -> ruoyi-system` 依赖方向,跨订单集成服务放在 `ruoyi-admin`。 - 不创建独立 worktree,不修改用户全局 JDK 配置;验证命令临时使用 `C:\Users\qmj\.jdks\graalvm-jdk-21.0.7`。 --- ### Task 1: 创建时绑定收件账号并支持“我收的” **Files:** - Modify: `ruoyi-system/src/main/java/com/ruoyi/system/domain/flash/FlashDeliveryOrder.java` - Modify: `ruoyi-system/src/main/java/com/ruoyi/system/mapper/InfoUserMapper.java` - Modify: `ruoyi-system/src/main/java/com/ruoyi/system/mapper/flash/FlashDeliveryOrderMapper.java` - Modify: `ruoyi-system/src/main/resources/mapper/infouser/InfoUserMapper.xml` - Modify: `ruoyi-system/src/main/resources/mapper/flash/FlashDeliveryOrderMapper.xml` - Modify: `ruoyi-admin/src/main/java/com/ruoyi/app/flashdelivery/service/FlashDeliveryApplicationService.java` - Modify: `ruoyi-admin/src/main/java/com/ruoyi/app/flashdelivery/controller/FlashDeliveryUserController.java` - Modify: `ruoyi-admin/src/test/java/com/ruoyi/app/flashdelivery/service/FlashDeliveryApplicationServiceTest.java` - Modify: `ruoyi-admin/src/test/java/com/ruoyi/app/flashdelivery/controller/FlashDeliveryControllerContractTest.java` - Modify: `ruoyi-system/src/test/java/com/ruoyi/system/mapper/flash/FlashDeliveryMapperXmlTest.java` **Interfaces:** - Consumes: `delivery.phone` from `FlashDeliveryCreateRequest` and token-derived current user ID. - Produces: `FlashDeliveryOrder.receiverUserId`; `InfoUserMapper.selectOrdinaryUserIdsByNormalizedPhone(String)`; `FlashDeliveryOrderMapper.transitionByParticipant(...)`; `GET /system/flashDelivery/orders?page=1&size=10&role=receiver`; participant-aware detail and confirmation. - [x] **Step 1: Write receiver-binding and participant-access tests** Add service tests whose observable assertions are: ```java when(userMapper.selectOrdinaryUserIdsByNormalizedPhone("886912345678")) .thenReturn(List.of(88L)); fixture.service.create(7L, requestWithDeliveryPhone("+886 912-345-678")); assertEquals(88L, insertedOrder.getReceiverUserId()); when(userMapper.selectOrdinaryUserIdsByNormalizedPhone(anyString())) .thenReturn(List.of(88L, 99L)); fixture.service.create(7L, requestWithDeliveryPhone("+886 912-345-678")); assertNull(insertedOrder.getReceiverUserId()); fixture.service.userOrders(88L, 1, 10, "receiver"); assertTrue(capturedQuery.getSqlSegment().contains("receiver_user_id")); assertDoesNotThrow(() -> fixture.service.userDetail(88L, orderId)); assertThrows(ServiceException.class, () -> fixture.service.userCancel(88L, orderId, reasonRequest())); ``` Add Controller reflection tests requiring optional `@RequestParam String role`. Extend the mapper XML test to assert `receiverUserId` mapping and participant transition SQL. - [x] **Step 2: Run tests and verify RED** ```powershell $env:JAVA_HOME='C:\Users\qmj\.jdks\graalvm-jdk-21.0.7' $env:PATH="$env:JAVA_HOME\bin;$env:PATH" mvn -pl ruoyi-admin -am -Dtest='FlashDeliveryApplicationServiceTest,FlashDeliveryControllerContractTest,FlashDeliveryMapperXmlTest' -Dsurefire.failIfNoSpecifiedTests=false test ``` Expected: compilation or assertions fail because `receiverUserId`, role-aware list, participant transition and phone lookup do not exist. - [x] **Step 3: Implement the minimal receiver model and queries** Add: ```java private Long receiverUserId; ``` Add `InfoUserMapper` lookup returning at most two IDs so ambiguous matches are detectable: ```java List selectOrdinaryUserIdsByNormalizedPhone(@Param("normalizedPhone") String normalizedPhone); ``` The XML query must require `user_type='0'`, active/non-deleted account, normalize stored phone by removing whitespace, `+`, `-`, `(` and `)`, and use `LIMIT 2`. In `create`, resolve once before insert and set only when the returned list contains exactly one ID. Idempotent retries return the original order without resolving again. Change the list signature to: ```java IPage userOrders( Long userId, int pageNum, int pageSize, String role) ``` Accept only `sender` and `receiver`, default null/blank to `sender`, and apply `user_id` or `receiver_user_id` before `selectPage`. Detail allows either participant; cancellation remains creator-only. Replace `transitionByUser` with `transitionByParticipant` and participant-aware SQL: ```sql WHERE id = #{id} AND (user_id = #{userId} OR receiver_user_id = #{userId}) AND status = #{expectedStatus} ``` - [x] **Step 4: Run Task 1 tests and verify GREEN** Run the Step 2 command. Expected: all specified tests pass with zero failures and errors. --- ### Task 2: 骑手级 Redisson 锁和独占判定 **Files:** - Create: `ruoyi-admin/src/main/java/com/ruoyi/app/order/RiderDeliveryLockService.java` - Create: `ruoyi-admin/src/main/java/com/ruoyi/app/order/RiderDeliveryExclusivityService.java` - Create: `ruoyi-admin/src/test/java/com/ruoyi/app/order/RiderDeliveryLockServiceTest.java` - Create: `ruoyi-admin/src/test/java/com/ruoyi/app/order/RiderDeliveryExclusivityServiceTest.java` - Modify: `ruoyi-admin/src/main/resources/i18n/messages.properties` - Modify: `ruoyi-admin/src/main/resources/i18n/messages_zh_CN.properties` - Modify: `ruoyi-admin/src/main/resources/i18n/messages_zh_TW.properties` - Modify: `ruoyi-admin/src/main/resources/i18n/messages_en_US.properties` - Modify: `ruoyi-admin/src/main/resources/i18n/messages_vi.properties` **Interfaces:** - Consumes: `RedissonClient`, `IPosOrderService`, `FlashDeliveryOrderMapper`, rider ID and target service type. - Produces: `RiderDeliveryLockService.withLock(Long, LockedCall)`; `RiderDeliveryExclusivityService.assertCanAcceptFood(Long)`; `assertCanAcceptFlash(Long,String)`. - [x] **Step 1: Write failing lock lifecycle tests** Test the real lock wrapper behavior around a mocked external Redisson boundary: ```java when(redissonClient.getLock("lock:delivery:rider:123")).thenReturn(lock); when(lock.tryLock(3L, TimeUnit.SECONDS)).thenReturn(true); String result = service.withLock(123L, () -> "accepted"); assertEquals("accepted", result); ``` Verify through behavior that acquisition timeout, interruption and runtime Redis failure throw `ServiceException`; outside a transaction the lock releases in `finally`; with active transaction synchronization it remains held until `afterCompletion`. - [x] **Step 2: Write failing exclusivity tests** Cover these literal outcomes: ```text URGENT + active food -> reject URGENT + active normal flash -> reject URGENT + no active work -> allow normal flash + active URGENT -> reject food + active URGENT -> reject food + active normal flash -> allow ``` Active flash statuses are exactly `ACCEPTED` and `PICKED_UP`; active food uses `qs_id=riderId`, `delivery_status in (1,2)`, `state in (0,1,2)` and `after_sale_status=0`. - [x] **Step 3: Run tests and verify RED** ```powershell mvn -pl ruoyi-admin -am -Dtest='RiderDeliveryLockServiceTest,RiderDeliveryExclusivityServiceTest' -Dsurefire.failIfNoSpecifiedTests=false test ``` Expected: compilation fails because both services are absent. - [x] **Step 4: Implement lock and exclusivity services** `RiderDeliveryLockService` uses: ```java RLock lock = redissonClient.getLock("lock:delivery:rider:" + riderId); boolean acquired = lock.tryLock(3L, TimeUnit.SECONDS); ``` Do not pass a lease time so Redisson watchdog renews the lock. Register `TransactionSynchronization.afterCompletion` and unlock only when `lock.isHeldByCurrentThread()`; if no synchronization is active, release in `finally`. Translate timeout, interruption and Redis failure to the existing system-busy/operation-interrupted i18n errors. `RiderDeliveryExclusivityService` uses real MyBatis-Plus count queries and throws the new `flash.delivery.rider.exclusive.conflict` message when rules reject acceptance. - [x] **Step 5: Run Task 2 tests and verify GREEN** Run the Step 3 command. Expected: all specified tests pass with zero failures and errors. --- ### Task 3: 将独占规则接入闪送与外卖接单并完成交付文档 **Files:** - Modify: `ruoyi-admin/src/main/java/com/ruoyi/app/flashdelivery/service/FlashDeliveryApplicationService.java` - Modify: `ruoyi-admin/src/main/java/com/ruoyi/app/order/OrderLifecycleService.java` - Modify: `ruoyi-admin/src/test/java/com/ruoyi/app/flashdelivery/service/FlashDeliveryApplicationServiceTest.java` - Modify: `ruoyi-admin/src/test/java/com/ruoyi/app/order/OrderLifecycleServiceTest.java` - Modify: `updatesql/sql.md` - Modify: `docs/flash-delivery-app-api.md` - Modify: `specs/024-flash-delivery/quickstart.md` - Modify: `specs/024-flash-delivery/tasks.md` **Interfaces:** - Consumes: Task 1 participant model and Task 2 lock/exclusivity services. - Produces: both effective acceptance paths execute eligibility checks and order update under `lock:delivery:rider:{riderId}`; documented receiver role API and SQL migration. - [x] **Step 1: Write failing integration-level service tests** For flash acceptance, assert the returned order is accepted only when `withLock` invokes the callback and exclusivity allows it; assert exclusive conflict leaves `orderMapper.accept` uncalled. For food acceptance, assert `OrderLifecycleService.acceptDeliveryByRider` enters the same rider lock and checks `assertCanAcceptFood` before applying CAS. - [x] **Step 2: Run tests and verify RED** ```powershell mvn -pl ruoyi-admin -am -Dtest='FlashDeliveryApplicationServiceTest,OrderLifecycleServiceTest,RiderDeliveryLockServiceTest,RiderDeliveryExclusivityServiceTest' -Dsurefire.failIfNoSpecifiedTests=false test ``` Expected: assertions fail because acceptance methods do not invoke the shared lock and exclusivity services. - [x] **Step 3: Integrate both acceptance services** Wrap `FlashDeliveryApplicationService.accept` as: ```java return riderDeliveryLockService.withLock(riderId, () -> { FlashDeliveryOrder order = requireOrder(orderId); exclusivityService.assertCanAcceptFlash(riderId, order.getServiceType()); // existing conditional accept, log and response }); ``` Wrap the full body of `OrderLifecycleService.acceptDeliveryByRider` with the same lock and call `assertCanAcceptFood(riderId)` before `applyCas`. Keep the existing per-order conditional update; do not lock pickup or delivery operations. - [x] **Step 4: Record SQL and update integration docs** Append, but do not execute: ```sql ALTER TABLE flash_delivery_order ADD COLUMN receiver_user_id BIGINT NULL COMMENT '创建时匹配的收件用户ID' AFTER user_id, ADD KEY idx_flash_receiver_list (receiver_user_id, create_time); ``` Document `role=sender/receiver`, default `sender`, participant detail/signature rules, no historical claim, integer TWD, no payment and the UI copy “发布后等待附近骑手接单”. Mark T055–T060 complete only after their corresponding source and docs exist. - [x] **Step 5: Run focused tests and full module verification** ```powershell $env:JAVA_HOME='C:\Users\qmj\.jdks\graalvm-jdk-21.0.7' $env:PATH="$env:JAVA_HOME\bin;$env:PATH" mvn -pl ruoyi-admin -am -Dtest='FlashDeliveryApplicationServiceTest,FlashDeliveryControllerContractTest,FlashDeliveryMapperXmlTest,OrderLifecycleServiceTest,RiderDeliveryLockServiceTest,RiderDeliveryExclusivityServiceTest,FlashDeliveryI18nContractTest' -Dsurefire.failIfNoSpecifiedTests=false test mvn -pl ruoyi-admin -am -DskipTests package git diff --check ``` Expected: tests report zero failures/errors, package exits 0, and diff check produces no errors. - [x] **Step 6: Audit and commit** Inspect `git diff --name-only` and `git diff --cached --name-only`; stage only Phase 12 production, tests, five i18n bundles, SQL and flash-delivery documents. Commit with a Chinese subject. Do not push unless the user requests it.