# 闪送“我收的”与急送独占 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<Long> 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<FlashDeliveryUserOrderListView> 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<T>)`; `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.
