Browse Source

fix: complete self-delivery order routing

qmj 6 hours ago
parent
commit
37357504e6

+ 29 - 1
ruoyi-admin/src/main/java/com/ruoyi/app/order/DeliveryOrderNotificationService.java

@@ -51,8 +51,15 @@ public class DeliveryOrderNotificationService {
         this.merchantNotificationRouter = merchantNotificationRouter;
     }
 
-    /** 支付完成且仍待骑手接单时,通知附近可接单骑手。 */
+    /**
+     * 支付完成后的新外送订单通知:自配送订单通知商家,其余订单通知附近可接单骑手。
+     * 所有下单、支付回调与线下转账确认入口复用这里,避免遗漏自配送分流。
+     */
     public void notifyOrderAvailable(PosOrder order) {
+        if (isAvailableForSelfDeliveryMerchant(order)) {
+            notifySelfDeliveryOrderToMerchant(order);
+            return;
+        }
         if (!isAvailableForRider(order)) {
             return;
         }
@@ -143,7 +150,28 @@ public class DeliveryOrderNotificationService {
         });
     }
 
+    /** 自配送新单只允许通知实际门店及其归属商家,绝不进入骑手通知链路。 */
+    private void notifySelfDeliveryOrderToMerchant(PosOrder order) {
+        if (merchantNotificationRouter == null) {
+            return;
+        }
+        runAfterCommit(() -> {
+            String ddId = String.valueOf(order.getDdId());
+            String body = OrderPushBodyDto.getJson(ddId, String.valueOf(order.getState()), 0);
+            merchantNotificationRouter.sendStoreNotification(order.getMdId(), order.getShId(),
+                    "no.message.push.message", "no.message.push.new.order", body, ddId);
+        });
+    }
+
+    private boolean isAvailableForSelfDeliveryMerchant(PosOrder order) {
+        return isPaidUnassignedDeliveryOrder(order) && Integer.valueOf(1).equals(order.getSelfDelivery());
+    }
+
     private boolean isAvailableForRider(PosOrder order) {
+        return isPaidUnassignedDeliveryOrder(order) && !Integer.valueOf(1).equals(order.getSelfDelivery());
+    }
+
+    private boolean isPaidUnassignedDeliveryOrder(PosOrder order) {
         return order != null
                 && Long.valueOf(0L).equals(order.getType())
                 && Long.valueOf(0L).equals(order.getState())

+ 9 - 1
ruoyi-admin/src/main/java/com/ruoyi/app/order/UserOrderController.java

@@ -255,7 +255,6 @@ public class UserOrderController extends BaseController {
             posOrder.setParentDdId(input.getDdId().toString()); // 设置父订单ID
             posOrder.setCretim(new Date());
             // 根据item设置订单信息
-            posOrder.setShId(item.getShId());
             posOrder.setMdId(item.getMdId());
             posOrder.setShdzId(input.getShdzId());
             posOrder.setUserId(userId);
@@ -303,6 +302,8 @@ public class UserOrderController extends BaseController {
                throw new ServiceException(MessageUtils.message("no.mendian.not.exist"));
            }
            PosStore store = storeOptional.get();
+            // 保持已有客户端 shId 的兼容性;缺失时以实际门店归属回填,夜市摊位会自然归属夜市主。
+            posOrder.setShId(resolveOrderMerchantId(item.getShId(), store));
             posOrder.setLongitude(store.getLongitude());
             posOrder.setLatitude(store.getLatitude());
             // 029 商家自配送:外送单按门店时段判定并固化快照(即时=下单时刻;预约按 delryTime 时间段开始时刻)
@@ -378,6 +379,13 @@ public class UserOrderController extends BaseController {
         }
     }
 
+    /**
+     * 用户下单兼容旧客户端:已传 shId 时保持原值;未传时只能以实际门店归属确定收款商家。
+     */
+    static Long resolveOrderMerchantId(Long requestedShId, PosStore store) {
+        return requestedShId != null ? requestedShId : store.getUserId();
+    }
+
     //保存默认发票抬头(010 Phase 11:结账勾「同意设置为默认」;同一用户仅一条 is_default=1)
     private void saveDefaultInvoiceProfile(OrderCreateInput input, Long userId) {
         String choice = input.getInvoiceChoice();

+ 19 - 0
ruoyi-admin/src/test/java/com/ruoyi/app/order/DeliveryOrderNotificationServiceTest.java

@@ -58,6 +58,25 @@ class DeliveryOrderNotificationServiceTest {
         verify(positions, never()).getAcceptRiderList(order.getLongitude(), order.getLatitude(), 20);
     }
 
+    @Test
+    void routesPaidSelfDeliveryOrderToMerchantInsteadOfRiders() {
+        RiderPositionMapper positions = mock(RiderPositionMapper.class);
+        MerchantNotificationRouter router = mock(MerchantNotificationRouter.class);
+        DeliveryOrderNotificationService service = new DeliveryOrderNotificationService(
+                positions, mock(PushEventService.class), mock(IInfoUserService.class), router);
+        PosOrder order = availableOrder();
+        order.setSelfDelivery(1);
+        order.setMdId(9L);
+        order.setShId(101L);
+
+        service.notifyOrderAvailable(order);
+
+        verify(positions, never()).getAcceptRiderList(order.getLongitude(), order.getLatitude(), 20);
+        verify(router).sendStoreNotification(9L, 101L,
+                "no.message.push.message", "no.message.push.new.order",
+                OrderPushBodyDto.getJson("DD-DELIVERY", "0", 0), "DD-DELIVERY");
+    }
+
     @Test
     void userCancellationRoutesMerchantNotificationByActualOrderStore() {
         RiderPositionMapper positions = mock(RiderPositionMapper.class);

+ 18 - 0
ruoyi-admin/src/test/java/com/ruoyi/app/order/UserOrderControllerMerchantIdTest.java

@@ -0,0 +1,18 @@
+package com.ruoyi.app.order;
+
+import com.ruoyi.system.domain.PosStore;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class UserOrderControllerMerchantIdTest {
+
+    @Test
+    void keepsRequestedShIdButFallsBackToActualStoreOwnerWhenMissing() {
+        PosStore store = new PosStore();
+        store.setUserId(101L);
+
+        assertEquals(202L, UserOrderController.resolveOrderMerchantId(202L, store));
+        assertEquals(101L, UserOrderController.resolveOrderMerchantId(null, store));
+    }
+}

+ 1 - 1
specs/029-merchant-self-delivery/data-model.md

@@ -58,7 +58,7 @@ ALTER TABLE pos_order ADD COLUMN self_delivery TINYINT(1) NOT NULL DEFAULT 0 COM
 ### 4. user_billing(现有表,无 DDL,启用新 type 值)
 
 - 现有用途:type="0" 分成(商家/骑手,dedup 键 userId+type+ddId)、type="3" 到付用户账单。
-- **新增 type="5" = 自配送运费入账**(实现前 grep `setType("` 全库确认 "5" 未被 user_billing 语义占用)
+- **type="5" 固定为自配送运费入账**。原商家端“金额解冻”业务已停止,页面筛选和展示均以“自配送运费”为准;不新增账单类型,避免与商品分成 type="0" 的去重键冲突
 - 字段赋值:`user_id=pos_order.sh_id`(普通商家=商家、夜市摊位单=夜市主,见 research F3)、`amount=freight 全额`、`divvy=0`、`divvy_rate=0`、`user_type="1"`、`md_id`、`dd_id`、`state="0"`、`illustrate=自配送运费(i18n)`。dedup 键沿用 (userId, type, ddId)——type 不同不与商品分成冲突。
 - 到付自配送单:送达时用户账单(type="3")`payment_id=sh_id`(镜像骑手送达的 qsId 归因,research F13)。
 

+ 18 - 9
specs/029-merchant-self-delivery/delivery.md

@@ -7,13 +7,14 @@
 | 部分 | 状态 |
 |------|------|
 | 后端(配置/判定/隔离/配送操作/结算/推送/i18n) | ✅ 完成,43 个新增测试全绿;全仓 455/455 全绿零回归 |
-| 数据库 DDL | ✅ 已登记 `updatesql/sql.md`(2026-09-08 029 段,共 3 条)——**需开发者手动执行后方可部署** |
-| 商家端 PC(foodie-store) | ⛔ 未做——见下「范围调整」 |
-| 客户端 App / 商家手机 App | 外部配合(清单见下) |
+| 数据库 DDL | ✅ 已登记 `updatesql/sql.md`(2026-09-16 补登记 029 段,共 3 条)——**需开发者手动执行后方可部署** |
+| 商家端 PC(foodie-store) | ⚠️ 夜市主自配送设置页已完成;无订单列表/详情及配送操作入口 |
+| 客户端 App(msd-user) | ✅ 订单列表/详情已显示自配送标识及商家配送状态,并隐藏骑手入口 |
+| 商家手机 App | ⛔ 源码未在当前工作区,仍需外部接入配送操作 |
 
 ## 上线前置清单(开发者手动执行)
 
-1. 在数据库执行 `updatesql/sql.md` 中「2026-09-08 029 商家自配送」的 3 条 DDL(pos_store 2 列、pos_store_self_delivery_hours 建表、pos_order 1 列)。**未执行前部署新包会导致 pos_order 相关查询报列不存在**。
+1. 在数据库执行 `updatesql/sql.md` 中「2026-09-16 补登记商家自配送表结构(029-merchant-self-delivery)」的 3 条 DDL(pos_store 2 列、pos_store_self_delivery_hours 建表、pos_order 1 列)。**未执行前部署新包会导致 pos_order 相关查询报列不存在**。
 2. 执行后按 `quickstart.md` 场景 1-8 回归(服务启动 + 接口级冒烟)。
 3. (可选,T020 遗留)用有权限的账号跑下面的只读核验 SQL,确认夜市主门店行齐全;解析不到的夜市会自动回落骑手池(不报错,但那部分夜市自配送不生效):
 
@@ -27,9 +28,9 @@ WHERE p.is_stall=1 AND p.del_flag='0' AND p.night_market_id IS NOT NULL
 
 ## 范围调整:商家端界面落点(重要)
 
-执行 T033 探索时发现:**foodie-store(PC)没有门店信息页、也没有订单列表页**(仅商品/分类/促销/对账等 17 个管理页);门店信息与订单管理界面在商家手机 App(uni-app,不在本工作区)。营业时段编辑也在 App 端。
+执行 T033 探索时发现:**foodie-store(PC)没有既有的门店信息页和订单列表页**(仅商品/分类/促销/对账等管理页);因此已额外落地一个仅供夜市主使用的“自配送设置”页,但订单管理界面仍在商家手机 App(uni-app,不在本工作区)。
 
-因此 spec 中 FR-001(门店信息配置界面)与 FR-010(订单列表标识)的**界面部分**归入商家 App 外部配合;后端接口(`/chanting/store/get|saveMdSelfDeliveryHours`)与订单字段已就绪,App 端接入即可。是否在 PC 端新建门店设置/订单页面,与 clarify Q2(配送操作界面端覆盖)同属一个待决策问题,建议一并讨论后追加。
+因此 FR-001 的夜市主配置子集已由 PC “自配送设置”页承接;FR-010 的订单列表标识及配送操作仍归商家 App 外部配合。后端接口(`/chanting/store/get|saveMdSelfDeliveryHours`)与订单字段已就绪,App 端接入即可。是否在 PC 端新建订单页面,与 clarify Q2(配送操作界面端覆盖)同属一个待决策问题,建议一并讨论后追加。
 
 ## 外部配合清单(客户端接入)
 
@@ -38,8 +39,8 @@ WHERE p.is_stall=1 AND p.del_flag='0' AND p.night_market_id IS NOT NULL
 - 订单列表/详情:`selfDelivery==1` 显示"自配送"标签,不显示等待骑手接单状态
 - 配送操作(开始配送/确认送达,可与 Q2 决策合并):`POST /system/orderShOprate/selfDeliveryStart` / `selfDeliveryComplete`(DTO: {id, qsImg?});开始配送为可选步骤
 
-**客户 App(uni-app)**:
-- 订单对象已自动携带 `selfDelivery` 字段(0/1);`selfDelivery==1` 显示"商家自配送",配送状态映射:deliveryStatus 0=备餐中、2=商家配送中、3=已送达(无骑手接单环节)
+**客户 App(uni-app,`msd-user`)**:
+- 已接入订单对象的 `selfDelivery` 字段(0/1):列表与详情显示"商家自配送";deliveryStatus 0/1=商家备餐中、2=商家配送中、3=已送达(无骑手接单环节)。自配送订单隐藏联系骑手入口与骑手地图路径。
 - 推送文案 key:`no.message.push.merchant.delivery.start` / `no.message.push.merchant.delivery.complete`(5 个 properties 已含四语言)
 
 ## 实现要点(代码导航)
@@ -61,7 +62,15 @@ WHERE p.is_stall=1 AND p.del_flag='0' AND p.night_market_id IS NOT NULL
 
 已知隐患(本期不改,记录备查):平台管理端 `GET /system/order/list` 走 XML 显式列清单不含 self_delivery 列——管理端界面按 D10 不在本期范围,未来管理端需要展示自配送标识时须同步 PosOrderMapper.xml 的 selectPosOrderVo。
 
-修复后全量回归:458/458 全绿。
+## 补正记录(2026-09-16)
+
+1. 029 的 3 条 DDL 曾缺失于 `updatesql/sql.md`,现已补登记;仍由开发者手动执行,应用不会自动迁移数据库。
+2. 已支付自配送外送单的创建、LINE Pay/OMG 支付成功及线下转账确认均复用 `DeliveryOrderNotificationService.notifyOrderAvailable`。该公共入口现统一分流:自配送订单仅通知门店/夜市主,其他外送订单才通知骑手;新增回归测试覆盖,避免骑手收到无效新单推送。
+3. `user_billing.type="5"` 固定表示自配送运费收入;已停止的“金额解冻”业务不再占用该类型。商家端筛选与列表显示统一为“自配送运费”(四语言),骑手和商家商品分成仍沿用 type="0"。
+4. 用户下单请求未传 `shId` 时,服务端以实际 `mdId` 对应门店的 `userId` 回填;已传 `shId` 保持旧客户端兼容行为。夜市摊位门店的 `userId` 为夜市主,因此自配送运费归属保持正确。
+5. 用户端 `msd-user` 订单列表和详情已接入自配送标识、状态文案与骑手入口隔离;八个现有语言包均已补齐。商家手机 App 的订单页面源码不在当前工作区,仍须接入 `/system/orderShOprate/selfDeliveryStart|selfDeliveryComplete`。
+
+历史全量回归记录保留于原始交付;本次补正仅执行定向后端测试、商家端生产构建和用户端语言 JSON 解析,详见本次提交说明。
 
 ## 遗留决策
 

+ 3 - 0
specs/029-merchant-self-delivery/tasks.md

@@ -186,3 +186,6 @@ description: "Task list for 029-merchant-self-delivery"
 - [x] T041 【2026-09-10 变更】普通门店保存自配送配置改为一律拒绝:仅夜市主门店行且夜市主本人可保存;新增提示 `no.store.selfdelivery.nightmarket.only`(六语言,th_TH 顺带补本 key);测试 `PosStoreControllerSelfDeliveryTest.normalStoreCannotSaveSelfDeliveryConfig`(原 normalStoreSaveGoesThroughStoreAccess 断言翻转)
 
 - [x] T042 【2026-09-10 追加】夜市主 PC 自配送设置入口:后端 my 读写接口(token 反查门店行+六语言拒绝文案+3 个控制器测试);foodie-store 新页面/路由/菜单/API/四语言 i18n
+
+- [x] T043 【2026-09-16 补正】补登记 3 条自配送 DDL;已支付自配送订单统一改为通知门店/夜市主而非骑手;`type=5` 账单显示统一为自配送运费;用户下单缺失 `shId` 时按已解析的门店归属回填;`msd-user` 订单列表/详情补自配送标识、商家配送状态和骑手入口隔离。
+- [ ] T044 【外部配合】商家手机 App 的订单页面源码不在当前工作区:接入自配送标签及 `selfDeliveryStart`/`selfDeliveryComplete` 操作入口后,才能完成商家侧实际操作闭环。

+ 23 - 0
updatesql/sql.md

@@ -1530,3 +1530,26 @@ ALTER TABLE flash_delivery_order
 ALTER TABLE flash_delivery_order
   ADD COLUMN pay_type VARCHAR(4) NOT NULL DEFAULT '4' COMMENT '支付方式:4=现金,6=线下转账(下单时锁定)' AFTER currency;
 ```
+
+## 2026-09-16 补登记商家自配送表结构(029-merchant-self-delivery)
+
+> 用途:补齐 029 商家自配送上线所需字段和时段表。该脚本仅登记,由开发者备份后手动执行;存量订单默认走骑手配送,不回填为自配送。
+
+```sql
+ALTER TABLE pos_store
+  ADD COLUMN self_delivery_enabled TINYINT(1) NOT NULL DEFAULT 0 COMMENT '自配送开关(夜市主门店行开启=旗下摊位统一生效)',
+  ADD COLUMN self_delivery_mode TINYINT(1) DEFAULT NULL COMMENT '自配送时段模式:1=全天,2=自定义按星期';
+
+CREATE TABLE pos_store_self_delivery_hours (
+  id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
+  md_id INT NOT NULL COMMENT '配置主体门店id(夜市场景为夜市主门店行)',
+  day_of_week TINYINT NOT NULL COMMENT 'ISO星期:1=周一..7=周日',
+  start_time VARCHAR(5) NOT NULL COMMENT '开始时刻 HH:mm(含)',
+  end_time VARCHAR(5) NOT NULL COMMENT '结束时刻 HH:mm(不含)',
+  PRIMARY KEY (id),
+  UNIQUE KEY uk_sd_hours (md_id, day_of_week, start_time)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='门店自配送时段(按星期)';
+
+ALTER TABLE pos_order
+  ADD COLUMN self_delivery TINYINT(1) NOT NULL DEFAULT 0 COMMENT '1=商家自配送(下单时快照,不进骑手池,运费归sh_id)';
+```