Explorar el Código

明确闪送收件人订单与急送独占设计

qmj hace 1 día
padre
commit
e418cf4b24

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

@@ -33,12 +33,12 @@
 | GET | `/system/flashDelivery/home` | 返回当前时间存在运价时段的服务及摘要 `serviceType,pricingId,startTime,endTime,startingDistance,startingFare,distance,freight,pricingVersion,currency`;不返回内部更新人、时间或地图密钥 |
 | POST | `/system/flashDelivery/quote` | `serviceType,packageType,packageSize,pickup,delivery` |
 | POST | `/system/flashDelivery/orders` | 报价请求字段 + `clientRequestId,deliveryMode,scheduledPickupStartAt?,scheduledPickupEndAt?,pinRequired?,senderImageUrls?,userNote?`;服务端重算并幂等创建 |
-| GET | `/system/flashDelivery/orders` | `page,size`;仅本人,按创建时间倒序返回全部状态的用户列表摘要 |
-| GET | `/system/flashDelivery/orders/{id}` | 本人 App 订单详情;`data` 直接为订单字段,不返回原始状态日志 |
-| POST | `/system/flashDelivery/orders/{id}/cancel` | `{"reason":"行程变化"}`;仅待接/已接 |
-| POST | `/system/flashDelivery/orders/{id}/confirmReceipt` | 无 body;仅 DELIVERED |
+| GET | `/system/flashDelivery/orders` | `page,size,role?`;`role=sender/receiver`,省略为 `sender`;按创建时间倒序返回当前用户作为寄件人或已绑定收件人的订单摘要 |
+| GET | `/system/flashDelivery/orders/{id}` | 订单创建人或创建时已绑定收件人的 App 订单详情;`data` 直接为订单字段,不返回原始状态日志 |
+| POST | `/system/flashDelivery/orders/{id}/cancel` | `{"reason":"行程变化"}`;仅订单创建人且状态为待接/已接 |
+| POST | `/system/flashDelivery/orders/{id}/confirmReceipt` | 无 body;订单创建人或已绑定收件人可对 DELIVERED 订单确认签收 |
 
-用户列表摘要字段固定为:`id,orderNo,serviceType,status,packageType,deliveryMode,scheduledPickupStartAt,scheduledPickupEndAt,pickupAddress,pickupDetailAddress,deliveryAddress,deliveryDetailAddress,estimatedDurationSeconds,amount,currency,deliveredAt,createTime`。列表不返回联系人、电话、精确坐标、照片、日志或计价审计字段。
+用户列表摘要字段固定为:`id,orderNo,serviceType,status,packageType,deliveryMode,scheduledPickupStartAt,scheduledPickupEndAt,pickupAddress,pickupDetailAddress,deliveryAddress,deliveryDetailAddress,estimatedDurationSeconds,amount,currency,deliveredAt,createTime`。列表不返回联系人、电话、精确坐标、照片、日志或计价审计字段。`receiver` 不是按当前手机号动态查询:订单只在创建时唯一匹配已注册普通用户并固化 `receiverUserId`,未匹配订单以后不追溯认领。
 
 报价响应 data:`serviceType,pricingId,startTime,endTime,distanceMeters,distanceSource,estimatedDurationSeconds?,startingDistance,startingFare,distance,freight,billableDistance,distanceFee,amount,currency,pricingVersion`。`billableDistance` 为按外卖规则处理 0.5 公里边界后的计费公里数,`distanceFee` 与 `amount` 为整数 TWD。地址快照字段为联系人、电话、市/区、交付方式、地址、详细地址和经纬度。客户端传入的金额/距离不会被使用。用户创建和详情响应的 `data` 直接为订单详情字段,照片分别为 `senderImageUrls,pickupImageUrls,deliveryImageUrls`,不返回原始状态日志;`deliveryPinCode` 仅在启用时向订单用户返回,用户仅在 `ACCEPTED`、`PICKED_UP` 阶段获得骑手位置。
 
@@ -73,6 +73,6 @@
 | POST | `/system/flashDelivery/admin/orders/{id}/cancel` | `reason` 必填;非终态可取消 |
 | POST | `/system/flashDelivery/admin/orders/{id}/complete` | 无 body;仅 DELIVERED |
 
-本期没有支付、退款、取消费、退回费、结算、骑手收入、代购/垫付、精确重量、件数、违禁品电子确认、骑手放弃或自动派单字段和接口。
+本期没有支付、退款、取消费、退回费、结算、骑手收入、代购/垫付、精确重量、件数、违禁品电子确认、骑手放弃或自动派单字段和接口。所有闪送金额保持整数 TWD;创建订单后直接待接单,不存在待支付状态。`URGENT` 为 1 对 1 独占配送,相关外卖与闪送接单入口必须执行跨订单独占检查。
 
 运价时段使用业务当地时间和 `HH:mm` 格式,采用开始时间包含、结束时间不包含的匹配语义;`24:00` 只允许作为结束时间,跨午夜时段必须拆成两个配置。同一服务类型的时间段不得重叠。起送距离和计价距离必须为正数,起送价格和计价金额必须为正整数;当前时间没有匹配时段时,用户首页不返回该服务,报价和创建接口返回“当前时段暂无可用运价”。

+ 2 - 2
specs/024-flash-delivery/data-model.md

@@ -2,9 +2,9 @@
 
 ## `flash_delivery_order`
 
-核心字段:`id`、唯一 `order_no`、与用户联合唯一的 `client_request_id`、`user_id`、`rider_id`、`service_type`、`status`;`package_type`、`package_size`;`delivery_mode`、预约开始/结束时间;`pin_required`、四位交付 PIN;取件/收件联系人、电话、市/区、交付方式、地址、详细地址和经纬度快照;`distance_meters`、`distance_source`、`estimated_duration_seconds`;整数 `amount`、`currency`、`pricing_id`、`pricing_version`、`pricing_start_time`、`pricing_end_time`、`starting_distance`、`starting_fare`、`distance`、`freight`、`distance_fee` 计价快照;`user_note`、`version`、关键状态时间、取消审计及创建/更新时间。
+核心字段:`id`、唯一 `order_no`、与用户联合唯一的 `client_request_id`、创建人 `user_id`、创建时按收件手机号唯一匹配并固化的可空 `receiver_user_id`、`rider_id`、`service_type`、`status`;`package_type`、`package_size`;`delivery_mode`、预约开始/结束时间;`pin_required`、四位交付 PIN;取件/收件联系人、电话、市/区、交付方式、地址、详细地址和经纬度快照;`distance_meters`、`distance_source`、`estimated_duration_seconds`;整数 `amount`、`currency`、`pricing_id`、`pricing_version`、`pricing_start_time`、`pricing_end_time`、`starting_distance`、`starting_fare`、`distance`、`freight`、`distance_fee` 计价快照;`user_note`、`version`、关键状态时间、取消审计及创建/更新时间。
 
-索引:订单号唯一、用户幂等号唯一、待抢排序、用户列表、骑手列表、自动完成扫描
+索引:订单号唯一、用户幂等号唯一、待抢排序、创建人列表、收件人列表、骑手列表、自动完成扫描。`receiver_user_id` 只保存创建当时唯一匹配的普通用户;空值和历史订单不补写,用户手机号变更也不回写
 
 ## `flash_delivery_pricing`
 

+ 23 - 0
specs/024-flash-delivery/design.md

@@ -214,3 +214,26 @@ COMPLETED
 - App 与后端同步切换到新接口,不保留测试阶段旧 `/available`、`/mine` 路由或旧参数兼容层。
 - `page`、`size` 或 `tab` 非法时返回国际化业务错误;坐标必须成对出现并通过经纬度范围校验。
 - 列表必须先按页签和角色过滤,再由数据库分页;禁止查出完整集合后在 Java 或客户端过滤。
+
+## 11. 2026-09-03 蓝湖稿对齐增量
+
+### 11.1 已确认边界
+
+- 闪送不接入支付;订单创建后直接进入 `WAITING_ACCEPTANCE`,蓝湖中的待支付、去支付和支付倒计时不进入接口。
+- 金额继续使用整数 TWD,蓝湖小数价格作为视觉占位处理。
+- 不实现动态附近骑手数或预计接单分钟数,用户端文案统一为“发布后等待附近骑手接单”。
+- `URGENT` 按 1 对 1 专送实现骑手独占,不再只表示另一套运价。
+
+### 11.2 “我收的”身份绑定
+
+采用创建时快照方案。创建订单时将收件手机号去除首尾/内部空白、`+`、`-` 和圆括号但保留国家码数字,并以同样规则匹配当时已注册的普通用户:恰好匹配一名时保存其用户 ID 到 `receiver_user_id`;没有匹配或出现重复匹配时保存空值。该字段创建后不可因注册、手机号换绑或地址簿变化而自动改写。
+
+没有采用每次查询时按当前手机号动态匹配,因为手机号回收或换绑会让新持有人读取历史订单;也不实现注册后追溯认领,因为这需要独立的手机号归属证明、认领审计和撤销流程,超出本次范围。
+
+用户列表增加 `role=sender/receiver`,省略时保持原有 `sender` 行为。寄件人与绑定收件人都能查看详情和交付 PIN、确认签收;取消仍只允许订单创建人。列表和详情权限直接使用固化用户 ID,不使用请求中的手机号判断。
+
+### 11.3 急送独占与并发
+
+所有骑手接单入口先锁定同一 `info_user` 骑手行,再查询该骑手的有效外卖和闪送任务。抢急送时要求不存在未送达外卖及 `ACCEPTED/PICKED_UP` 闪送;抢普通外卖或普通闪送时,如果已有 `ACCEPTED/PICKED_UP` 急送则拒绝。急送进入 `DELIVERED`、`COMPLETED` 或 `CANCELLED` 后解除独占。
+
+同一骑手的外卖和闪送接单必须共享这把数据库行锁,确保并发请求不能分别在两张订单表中同时通过“无冲突”检查。

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

@@ -519,3 +519,28 @@ git push origin test
 ```
 
 The staged list must not contain `.claude/homunculus/observations.jsonl`、`.tmp/` or `docs/merchant-subaccount-api.md`.
+
+## 新版蓝湖稿增量计划(2026-09-04)
+
+### 目标
+
+保持闪送非支付和整数 TWD 现状,新增创建时收件账号绑定与“我收的”查询,并将 `URGENT` 落实为跨外卖、闪送任务的骑手独占。附近骑手人数和接单分钟数不进入接口,用户端统一展示“发布后等待附近骑手接单”。
+
+### 数据与身份
+
+1. `flash_delivery_order` 增加可空 `receiver_user_id BIGINT` 和 `(receiver_user_id, create_time)` 索引。
+2. 创建订单时将 `delivery.phone` 去除空白、`+`、`-` 和圆括号但保留国家码数字,并以相同规则只查询已注册普通用户;唯一匹配时固化用户 ID,无匹配或多匹配时留空。
+3. 不扫描或回填历史订单,不因用户注册、注销或手机号换绑修改 `receiver_user_id`。
+4. 用户列表新增可选 `role`,默认 `sender`;`receiver` 使用 `receiver_user_id` 在数据库分页前过滤。
+5. 详情和确认签收允许创建人或绑定收件人;取消只允许创建人。
+
+### 急送独占
+
+1. 为骑手用户增加 `SELECT ... FOR UPDATE` 行锁方法,闪送接单和当前有效外卖接单入口在事务内首先获取同一行锁。
+2. 抢 `URGENT` 前检查该骑手没有未送达外卖,也没有 `ACCEPTED/PICKED_UP` 闪送。
+3. 抢其他任务前检查该骑手没有 `ACCEPTED/PICKED_UP` 的 `URGENT` 闪送。
+4. 独占冲突使用统一国际化业务错误;条件更新仍作为订单被他人抢走的最终并发保护。
+
+### 测试与交付
+
+先写并运行失败测试,再实现生产代码。测试覆盖手机号唯一/零个/多个匹配、幂等创建、角色分页、参与者权限、取消边界、急送双向互斥、终态释放和同骑手并发接单。数据库变更只追加到 `updatesql/sql.md`,不直接执行。最后使用 JDK 21 运行定向测试和模块构建,并执行 `git diff --check`。

+ 34 - 4
specs/024-flash-delivery/spec.md

@@ -6,7 +6,7 @@
 
 **状态**:平台前端与外卖同规则时段运价调整已验证;App 订单列表与详情精简已提交,定向测试与模块构建通过
 
-**输入**:基于蓝湖“闪送”分组 7 个设计页面及 2026-08-24 用户端/骑手端补充原型,实现帮送、帮取、加急送的完整非支付业务闭环,包括共享地址簿、路线报价、包裹信息、立即/预约配送、可选 PIN 交付、创建订单、骑手主动抢单、取件、送达、用户签收、平台配置与介入。
+**输入**:基于蓝湖“闪送”分组 7 个设计页面、2026-08-24 用户端/骑手端补充原型及 2026-09-03 更新的 19 张用户端设计稿,实现帮送、帮取、加急送的完整非支付业务闭环,包括共享地址簿、路线报价、包裹信息、立即/预约配送、可选 PIN 交付、创建订单、寄件人/收件人订单查询、骑手主动抢单、1 对 1 急送独占、取件、送达、用户签收、平台配置与介入。
 
 ## 用户场景与测试
 
@@ -86,8 +86,12 @@
 2. **假如** 订单已取件,**当** 订单用户尝试取消,**那么** 系统拒绝并要求平台介入。
 3. **假如** 骑手已确认送达,**当** 订单用户确认签收,**那么** 订单进入已完成状态。
 4. **假如** 骑手送达后用户 24 小时未确认,**当** 自动完成任务执行,**那么** 订单进入已完成状态并记录自动操作日志。
-5. **假如** 用户访问其他用户的订单,**当** 其查询详情、取消或签收,**那么** 系统拒绝访问。
-6. **假如** 用户查询订单列表,**当** 仅提交 `page` 和 `size`,**那么** 系统按创建时间倒序返回本人各状态订单的轻量摘要,不要求客户端提交 `scene`、`status` 或 `serviceType`。
+5. **假如** 用户既不是订单创建人也不是已绑定收件人,**当** 其查询详情、取消或签收,**那么** 系统拒绝访问。
+6. **假如** 用户查询订单列表,**当** 提交 `role=sender` 或省略 `role`,**那么** 系统按创建时间倒序返回本人创建的各状态订单轻量摘要,不要求客户端提交 `scene`、`status` 或 `serviceType`。
+7. **假如** 创建订单时收件手机号唯一匹配一名当时已注册的普通用户,**当** 该用户提交 `role=receiver` 查询列表或查询订单详情,**那么** 系统返回该用户作为收件人的订单并允许读取收件码和配送进度。
+8. **假如** 创建订单时收件手机号没有匹配账号或匹配结果不唯一,**当** 手机号对应用户以后注册或发生换绑,**那么** 历史订单不自动认领或改变收件账号归属。
+9. **假如** 匹配收件人查询订单,**当** 尝试取消订单,**那么** 系统拒绝操作;寄件人仍是唯一有权主动取消订单的用户。
+10. **假如** 寄件人或匹配收件人确认已收货,**当** 订单处于已送达状态,**那么** 系统以原子状态转换完成订单。
 
 ---
 
@@ -126,6 +130,23 @@
 5. **假如** 订单处于非终态,**当** 具有取消权限的管理员填写原因并二次确认,**那么** 页面调用平台取消接口并刷新列表与详情。
 6. **假如** 订单处于已送达状态,**当** 具有完成权限的管理员二次确认,**那么** 页面调用平台完成接口并刷新订单状态。
 
+---
+
+### 用户故事 7:收件人查看订单且急送骑手保持独占(优先级:P1)
+
+已注册普通用户在他人创建订单时通过收件手机号被安全绑定为收件人,可在“我收的”查看订单详情和配送进度;骑手承接 1 对 1 急送后,在订单送达或取消前不得承接其他外卖或闪送任务。
+
+**独立测试**:使用已注册、未注册、重复手机号和换绑手机号创建订单,验证收件账号只在创建时唯一匹配并固化;并发发起急送与其他任务抢单,验证同一骑手只能进入一个互斥的有效接单结果。
+
+**验收场景**:
+
+1. **假如** 收件手机号唯一匹配一名已注册普通用户,**当** 创建订单,**那么** 系统将该用户 ID 固化为 `receiverUserId`,后续手机号变化不改写历史订单。
+2. **假如** 收件手机号没有匹配或匹配到多名用户,**当** 创建订单,**那么** 订单照常创建但 `receiverUserId` 为空,不向任何账号开放“我收的”权限。
+3. **假如** 骑手已有未送达的外卖或有效闪送任务,**当** 抢 1 对 1 急送订单,**那么** 系统拒绝接单。
+4. **假如** 骑手已承接 1 对 1 急送且订单仍处于已接单或已取件,**当** 再抢外卖、普通闪送或另一笔急送,**那么** 系统拒绝接单。
+5. **假如** 急送订单已送达、完成或取消,**当** 骑手再次抢单,**那么** 独占限制解除。
+6. **假如** 急送与其他任务并发抢单,**当** 请求针对同一骑手到达,**那么** 所有相关接单入口使用同一骑手行锁串行检查,不能绕过独占规则。
+
 ## 边界情况
 
 - 取件地址和收件地址相同或坐标相同,不允许创建订单。
@@ -207,6 +228,15 @@
 - **FR-058**:骑手列表 `tab` 只接受 `newTask`、`toPickup`、`delivering`、`completed` 和 `cancelled`,分别映射待接单、已接单、已取件、已送达/已完成和已取消;闪送不提供外卖退款页签。
 - **FR-059**:骑手 `newTask` 列表必须返回取件点距离、路线距离、预计时长、订单金额、包裹类别、重量档、配送方式、预约时段、完整取送文字地址和是否需要 PIN,并返回附近任务总数与最高订单金额;不得返回已移除的尖峰倍率或尚未实现的骑手收入。
 - **FR-060**:用户创建订单、用户详情、骑手详情和骑手接单成功响应必须使用直接且稳定的 App 订单详情结构;权限差异只影响敏感字段是否返回,不得改变 `data` 的顶层形状,也不得要求 App 通过 `data.order` 是否存在判断响应类型。
+- **FR-061**:订单新增可空 `receiver_user_id`;创建订单时将收件手机号去除首尾/内部空白、`+`、`-` 和圆括号但不改写国家码,再以剩余数字唯一匹配当时已注册且 `userType=0` 的普通用户并固化匹配结果,不执行历史订单追溯认领。
+- **FR-062**:手机号无匹配或存在多个普通用户匹配时不得绑定收件账号;订单仍可创建,且不得选择任意一名用户作为收件人。
+- **FR-063**:用户订单列表新增 `role` 参数,只接受 `sender` 和 `receiver`,省略时兼容为 `sender`;筛选必须在数据库分页前完成。
+- **FR-064**:订单创建人和已绑定收件人均可查询订单详情并确认签收;只有订单创建人可以主动取消订单。交付 PIN 对两类参与者可见。
+- **FR-065**:`URGENT` 表示 1 对 1 独占配送。骑手已有未送达外卖、`ACCEPTED` 或 `PICKED_UP` 闪送时不得接急送;骑手存在 `ACCEPTED` 或 `PICKED_UP` 急送时不得再接任何外卖或闪送。
+- **FR-066**:所有有效骑手接单入口必须先锁定同一骑手用户行,再检查跨外卖和闪送订单的独占条件并执行接单,避免并发绕过。
+- **FR-067**:闪送继续保持非支付业务,创建成功直接进入 `WAITING_ACCEPTANCE`,不增加待支付状态、支付倒计时、支付接口或退款流程。
+- **FR-068**:所有闪送金额继续使用整数 TWD;蓝湖稿中的小数金额仅为视觉占位,不改变接口和数据库金额类型。
+- **FR-069**:用户端不得展示固定或伪造的附近骑手人数及预计接单分钟数,统一使用“发布后等待附近骑手接单”;本期不增加实时骑手数量或接单预测接口。
 
 ### 关键实体
 
@@ -237,7 +267,7 @@
 - 地图路线服务的具体供应商与密钥沿用项目现有配置能力;计划阶段确定现有配置键和 HTTP 调用位置。
 - 币种为新台币,接口使用稳定币种代码 `TWD`。
 - 第一阶段不根据有效骑手数量阻止下单,也不实现自动匹配、自动派单或定向派单。
-- 蓝湖页面中的附近骑手数量与预计接单时间第一阶段不作为动态业务承诺。
+- 蓝湖页面中的附近骑手数量与预计接单时间不作为动态业务承诺,用户端统一显示“发布后等待附近骑手接单”
 - 骑手第一阶段不能自行放弃已抢订单,由平台管理员介入处理异常订单。
 
 ## 非目标

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

@@ -94,3 +94,13 @@
 - 验证结果:JDK 21 定向测试 40 项全部通过(0 失败、0 错误);`mvn -pl ruoyi-admin -am -DskipTests package` 成功;`git diff --check` 清洁。
 - 另按审核结论补记 `sys_job` 闪送自动完成调度 SQL(见 `updatesql/sql.md` 2026-09-02 节),并在 `plan.md` 修订并发描述与实现一致。
 - 已有规格提交:`17888f8`(明确闪送 App 订单列表与详情契约)、`8a3ea64`(制定闪送 App 订单接口精简计划)。
+
+## Phase 12:新版蓝湖稿的收件人订单与急送独占
+
+- [ ] T055 更新规格、设计、数据模型和 API 契约,固定非支付、整数 TWD、静态等待接单文案、创建时收件账号匹配及急送独占规则
+- [ ] T056 先扩展 Mapper、应用服务和 Controller 测试,覆盖收件账号唯一匹配、无匹配、重复匹配、历史不追溯、`role` 列表、参与者详情/签收、收件人禁止取消,并确认旧实现红灯
+- [ ] T057 新增 `receiver_user_id` 实体/XML/索引和普通用户手机号精确匹配查询,实现创建时固化、寄件人/收件人数据库分页及参与者权限
+- [ ] T058 先补跨外卖/闪送接单测试,覆盖忙碌骑手拒绝急送、急送阻止其他接单、终态释放和并发串行检查,并确认旧实现红灯
+- [ ] T059 以统一骑手行锁实现 `URGENT` 跨订单独占,在当前有效外卖与闪送接单入口返回一致的国际化业务错误
+- [ ] T060 在 `updatesql/sql.md` 追加 `receiver_user_id` 字段和索引迁移,仅记录不执行;同步 App 接口文档和五语言错误文案
+- [ ] T061 使用 JDK 21 运行闪送、外卖接单和 i18n 定向测试、模块构建与差异检查,核对暂存范围后使用中文提交