Browse Source

fix(push): classify ready scene only on actual merchant dispatch

qmj 1 tuần trước cách đây
mục cha
commit
e351e02092

+ 8 - 3
ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderShOprateController.java

@@ -375,7 +375,7 @@ public class PosOrderShOprateController extends BaseController {
         // 出餐推送
         // 持久化失败不能通知“已出餐”;本次不改变原接口及发票处理的事务边界。
         if (saved) {
-            chuCan(order, update);
+            chuCan(order, update, UserNotificationScene.MERCHANT_READY);
         }
         // 出餐自动开票(010 Phase 11):按订单发票意图自动开,异常不阻断出餐,失败单由运营后台重试
         try {
@@ -768,9 +768,14 @@ public class PosOrderShOprateController extends BaseController {
     }
 
     /**
-     * 商家出餐推送:给用户和骑手推送通知
+     * 兼容旧调用的文字通知:资料编辑也会调用,不能隐式视作“商家已出餐”。
      */
     public void chuCan(PosOrder oldOrder, PosOrder posOrder) {
+        chuCan(oldOrder, posOrder, null);
+    }
+
+    /** 实际出餐入口明确传入场景;旧资料编辑沿用文字通知,不添加成功语音场景。 */
+    private void chuCan(PosOrder oldOrder, PosOrder posOrder, UserNotificationScene scene) {
         runNotificationAfterCommit(() -> {
             List<Long> ids = new ArrayList<>();
             ids.add(oldOrder.getUserId());
@@ -791,7 +796,7 @@ public class PosOrderShOprateController extends BaseController {
             if (user.isPresent()) {
                 // 用户场景单独构造,下面骑手仍使用原 body。
                 String userBody = OrderPushBodyDto.getUserJson(String.valueOf(oldOrder.getDdId()), stateStr,
-                        -1, 1, UserNotificationScene.MERCHANT_READY);
+                        -1, 1, scene);
                 InfoUser u = user.get();
                 PayPush push = new PayPush();
                 Long uUserId = u.getUserId();

+ 13 - 3
ruoyi-admin/src/test/java/com/ruoyi/app/order/PosOrderShOprateControllerTest.java

@@ -373,9 +373,19 @@ class PosOrderShOprateControllerTest {
         ReflectionTestUtils.setField(controller, "infoUserService", users);
         ReflectionTestUtils.setField(controller, "businessPushService", push);
 
-        ReflectionTestUtils.invokeMethod(controller, "pushSelfDeliveryStatus",
-                selfDeliveryOrder(2L, 1L), "content.key", "2",
-                com.ruoyi.app.push.UserNotificationScene.ORDER_DELIVERING);
+        // 独立运行本测试时也提供异步管理器所需 Bean,不能依赖其他测试的类初始化顺序。
+        try (var beans = org.mockito.Mockito.mockStatic(com.ruoyi.common.utils.spring.SpringUtils.class)) {
+            beans.when(() -> com.ruoyi.common.utils.spring.SpringUtils.getBean("scheduledExecutorService"))
+                    .thenReturn(mock(java.util.concurrent.ScheduledExecutorService.class));
+            var manager = mock(com.ruoyi.framework.manager.AsyncManager.class);
+            try (var async = org.mockito.Mockito.mockStatic(com.ruoyi.framework.manager.AsyncManager.class)) {
+                async.when(com.ruoyi.framework.manager.AsyncManager::me).thenReturn(manager);
+                ReflectionTestUtils.invokeMethod(controller, "pushSelfDeliveryStatus",
+                        selfDeliveryOrder(2L, 1L), "content.key", "2",
+                        com.ruoyi.app.push.UserNotificationScene.ORDER_DELIVERING);
+                verify(manager).execute(any(java.util.TimerTask.class));
+            }
+        }
 
         // 是否外发交给通道判断,不在用户消息入库之前读取设备地址。
         verify(push, never()).addressKey(recipient);

+ 20 - 1
ruoyi-admin/src/test/java/com/ruoyi/app/order/UserOrderNotificationSceneTest.java

@@ -236,11 +236,30 @@ class UserOrderNotificationSceneTest {
     @Test
     void readyNotificationOnlyAddsSceneToUserAndWaitsForCommit() {
         PosOrderShOprateController controller = wire(new PosOrderShOprateController());
+        ReflectionTestUtils.setField(controller, "linePayOrderGuard", mock(LinePayOrderGuard.class));
+        ReflectionTestUtils.setField(controller, "orderInvoiceService", mock(OrderInvoiceService.class));
         InfoUser rider = new InfoUser(); rider.setUserId(88L); rider.setUserType("2");
         when(users.list(any(com.baomidou.mybatisplus.core.conditions.Wrapper.class))).thenReturn(List.of(user, rider)); order.setQsId(88L);
-        controller.chuCan(order, order);
+        order.setState(1L);
+        controller.dispatchOrder(JwtUtil.token("101", "owner"), 10L);
         assertTrue(userBodies.isEmpty()); assertTrue(riderBodies.isEmpty()); commit();
         assertEquals("MERCHANT_READY", userBodies.get(0).getString("eventScene"));
         assertEquals(1, riderBodies.size()); assertFalse(riderBodies.get(0).containsKey("eventScene"));
     }
+
+    @ParameterizedTest
+    @ValueSource(longs = {0, 1, 2})
+    void legacyInformationEditDoesNotPretendMerchantReady(long originalState) {
+        PosOrderShOprateController merchantController = wire(new PosOrderShOprateController());
+        PosOrderController legacyController = wire(new PosOrderController());
+        ReflectionTestUtils.setField(legacyController, "shOprateController", merchantController);
+        when(users.list(any(com.baomidou.mybatisplus.core.conditions.Wrapper.class))).thenReturn(List.of(user));
+        order.setState(originalState); order.setCollectPayment("0");
+        PosOrder edit = new PosOrder(); edit.setId(10L); edit.setCollectPayment("0"); // No state change.
+        ReflectionTestUtils.invokeMethod(legacyController, "setOrderNoState", edit, "88");
+        assertTrue(userBodies.isEmpty()); commit();
+        assertEquals(1, userBodies.size()); // Preserve the existing text notification, without inventing success.
+        assertFalse(userBodies.get(0).containsKey("eventScene"));
+        assertEquals(String.valueOf(originalState), userBodies.get(0).getString("state"));
+    }
 }

+ 3 - 3
specs/035-user-notification-voice/checklists/requirements.md

@@ -2,7 +2,7 @@
 
 **Created:** 2026-10-09。**Feature:** [spec.md](../spec.md)。
 
-**进度:**实现中。以下为文档自检,不是生产代码测试或真机验证。
+**进度:**已验证(本期后端)。以下为文档自检,实际代码验证及全量回归的基线失败另记 tasks;不是云端或真机验证。
 
 - [x] CHK001 仅现有用户通知新增 eventScene,不另发语音通知。
 - [x] CHK002 七场景及外卖/闪送/自配送实际触发边界明确。
@@ -15,8 +15,8 @@
 - [x] CHK009 spec、plan、tasks、接入文档字段与方法一致,无已上线声明。
 - [x] CHK010 2026-10-09 原书面规格已确认,随后边界按用户反馈修订。
 - [x] CHK011 用户已确认继续,按单字段精简计划原分支实施。
-- [x] CHK012 后端 149 项定向测试及模块构建通过;真实联调未执行,已明确交接,不以文档检查替代。
+- [x] CHK012 后端 152 项定向测试及模块构建通过;全量 1 项基线失败、真实联调未执行均明确记录,不以文档检查替代。
 
 ## 变更记录
 
-2026-10-09 已由前后台语音全流程收缩到前台,并排除云函数;之后按用户要求 App 由前端人员负责,只交付后端与接入文档。用户确认 eventScene 的用途并拒绝其他新增字段,随后确认继续实施。后端已接入,统一验证收口中;分支未创建/切换。
+2026-10-09 已由前后台语音全流程收缩到前台,并排除云函数;之后按用户要求 App 由前端人员负责,只交付后端与接入文档。用户确认 eventScene 的用途并拒绝其他新增字段,随后确认继续实施。后端已接入,定向验证、构建及审查问题修复完成;全量回归基线失败另记,分支未创建/切换。

+ 4 - 2
specs/035-user-notification-voice/frontend-integration.md

@@ -2,7 +2,7 @@
 
 **日期:**2026-10-09。
 
-**状态:**后端已验证,未部署;本地七场景接入、149 项定向测试及模块构建通过。App 实现与真实云端/真机联调由前端完成;示例不代表线上已返回该字段。
+**状态:**后端已验证,未部署;本地七场景接入、152 项定向测试及模块构建通过。App 实现与真实云端/真机联调由前端完成;示例不代表线上已返回该字段。
 
 ## 责任与范围
 
@@ -41,6 +41,8 @@
 
 不增加通知时机:闪送部分取件不发派送,当前闪送取消只通知骑手,本期不补用户取消;自配送直接送达不补造开始配送。平台旧入口的数字 state 含义不同,不能用 state=3 一律判断送达。
 
+只有实际商家出餐动作携带 MERCHANT_READY。旧订单资料编辑仍保留其原文字通知,但不携带 eventScene;前端不得据此或根据旧正文补播“已出餐”。
+
 ## 示例(后端协议示例,未部署)
 
 ```json
@@ -82,4 +84,4 @@
 
 ## 当前验证记录
 
-七场景已接入,JDK 21 统一定向测试 149/149 通过,`mvn -pl ruoyi-admin -am -DskipTests package` 为 BUILD SUCCESS;整体审查和扩展回归另记于 tasks。App、插件、音频、云函数未修改,真实云端/部署和真机未验证。新字段仅在完整 payload/body 内,不应从顶层查找 eventScene。
+七场景已接入,JDK 21 统一定向测试 152/152 通过,修复后 `mvn -pl ruoyi-admin -am -DskipTests package` 为 BUILD SUCCESS;整体审查发现的旧编辑误分类已回归修复。扩展回归 admin 947 项仍有 1 项基线泰文 key 缺失失败,详见 tasks。App、插件、音频、云函数未修改,真实云端/部署和真机未验证。新字段仅在完整 payload/body 内,不应从顶层查找 eventScene。

+ 8 - 1
specs/035-user-notification-voice/plan.md

@@ -10,7 +10,7 @@
 
 **Spec:** [spec.md](spec.md)
 
-**Date / Branch / Progress:** 2026-10-09 / 当前 test-202609v2 / 实现中(用户已确认执行,最终验证收口中,未部署)。
+**Date / Branch / Progress:** 2026-10-09 / 当前 test-202609v2 / 已验证(本期后端测试、构建及审查修复通过;全量回归 1 项基线失败另记,未部署)。
 
 ## Global Constraints
 
@@ -109,3 +109,10 @@ git diff --check
 ## Dependencies / Handoff
 
 Task 1 → Task 2 → Task 3 → Task 4;详单见 tasks.md。用户已确认继续,按本计划顺序实施;不创建分支或 worktree。收口时依执行计划技能安排一次只读整体审查,不分派实现任务。
+
+## 最终执行结果
+
+- 实际业务动作明确传场景;共用旧资料编辑通知的二参 chuCan 不带场景,真正出餐入口调用私有三参版本传 MERCHANT_READY。
+- 定向测试 152/152、独立商家/场景测试 33/33、修复后模块 package BUILD SUCCESS,差异格式检查通过。
+- 一次只读整体审查仅发现上述误分类 Important,已按失败回归修复;无 Critical/Minor,不重新派发审查。
+- 扩展全模块测试的 admin 947 项中有 1 项基线泰文键集合失败,不修范围外语言包、不推送或部署。App/云端与真机验收交接前端。完整记录及实施取舍见 tasks.md。

+ 3 - 1
specs/035-user-notification-voice/spec.md

@@ -4,7 +4,7 @@
 
 **Created**: 2026-10-09
 
-**Status**: 实现中(单字段方案已确认并实施;最终构建与审查收口中,未部署)
+**Status**: 已验证(本期后端 152 项定向测试及模块构建通过,审查问题已修正;完整回归存在 1 项基线泰文语言包失败;未部署,App/真机未验证)
 
 **工作分支**:后端沿用 `test-202609v2`。不创建分支或 worktree,不切换、合并或改写其他分支;不修改任何 App 仓库。
 
@@ -171,3 +171,5 @@
 2026-10-09 用户确认 eventScene 的播放场景用途,并明确其他新增字段不要。已精简规格、计划、任务和接入文档:只保留一个新增场景字段,撤销对服务端事件 ID、语种、接收用户和时间的依赖。产品代码尚未开始。
 
 2026-10-09 用户确认继续后在原分支实施:用户专用 JSON 工厂仅增加 eventScene;骑手、自配送、出餐、LINE/OMG 支付和现有外卖取消接入七场景。无地址仍走消息入库,提交后通知与通知准备失败隔离;商家/骑手 body 保持原样。公共控制器测试集中在 UserOrderNotificationSceneTest,覆盖实际参数、回滚、失败、直接送达及旧状态兼容。未新增 SQL、云函数、App 或 PC 修改。
+
+最终审查发现旧资料编辑复用出餐通知:已改为由真正出餐入口明确传 MERCHANT_READY,旧编辑只保留历史文字通知、不加场景。原状态 0/1/2 的三例失败测试后通过;定向 152/152,独立商家/场景测试 33/33,修复后模块构建通过。完整 admin 回归 947 项中 946 通过、1 项基线泰文 key 缺失失败;详见 tasks.md,不声称全量回归全绿或 App 已上线。

+ 19 - 2
specs/035-user-notification-voice/tasks.md

@@ -2,7 +2,7 @@
 
 **Input:** [spec.md](spec.md)、[plan.md](plan.md)、[frontend-integration.md](frontend-integration.md)。
 
-**进度:** 实现中(后端接入及分批测试已完成,最终验证收口中)。本期只新增 eventScene;不修改 App、云函数、音频或数据库。任务按现有用户故事追踪,步骤/签名/断言以 plan 为准;控制器公共夹具集中于 UserOrderNotificationSceneTest,其余旧控制器测试继续回归。
+**进度:** 已验证(本期后端测试/构建和审查修复通过;完整回归 1 项既有失败另记)。本期只新增 eventScene;不修改 App、云函数、音频或数据库。任务按现有用户故事追踪,步骤/签名/断言以 plan 为准;控制器公共夹具集中于 UserOrderNotificationSceneTest,其余旧控制器测试继续回归。
 
 ## Phase 1: 契约(Plan Task 1)
 
@@ -39,7 +39,24 @@
 - 通道与入库回归 49/49 通过;旧 payload、default sound、qsRing、pushsound.caf 保持。uni 仍转发完整 JSON,无证据需要修改 PayPush 参数编码,T013 按条件无需改代码。
 - JDK 21 最终统一定向测试 149/149 通过(包含 UserOrderNotificationSceneTest),模块 package 为 BUILD SUCCESS。无 SQL、App、云函数、PC 或通道生产配置变更。
 - 本地提交不自动 push;未部署、未生产试发,真实云端 payload 透传、App 音频/前台效果与真机尚未验证。
-- 额外全模块回归与一次只读整体审查将在收口时补记,不把定向测试当作完整 App 功能验收。
+- 额外执行 `mvn -pl ruoyi-admin -am test`:admin 944 项,943 通过、1 失败、0 错误。唯一失败为 BackendI18nBundleContractTest 的泰文键集合检查:缺少 10 个 stall.qualification key;开始修改前的 `9e4e5f` 提交已存在同样缺失,语言包及该测试本次未修改。按范围不夹带无关修复,不能宣称完整回归全绿。一次只读整体审查另补记;定向测试也不代表 App 功能验收。
+
+## 最终审查与修复验证
+
+- 一次只读审查范围 `9e4e5f3..9aff95d`:无 Critical/Minor,一项 Important 是旧资料编辑复用 chuCan 错带 MERCHANT_READY。
+- 已修正:实际 dispatchOrder 明确传场景;旧无状态编辑保留历史文字、不新增场景。真实 setOrderNoState 路径原状态 0/1/2 的三例回归 RED(3 失败、0 错误)后 GREEN;出餐测试改为验证实际 dispatchOrder,非用户 body 仍无场景。
+- 修复后统一定向测试 152/152;独立商家/场景测试 33/33,避免测试夹具依赖 AsyncManager 初始化顺序;修复后 module package BUILD SUCCESS。
+- 修复后再次全模块回归:admin 947 项,946 通过、1 失败、0 错误;唯一失败仍是上述基线泰文缺 10 个摊位审核 key。没有将这项失败隐藏或记作完整回归通过。
+- 最终未验证项:真实云端部署是否保留 payload、实际推送到达、App 音频/前台播放、后台与锁屏真机行为、生产环境联调。本期不修改这些部分。
+
+## 实施取舍及代价
+
+- 按用户要求在原 checkout 实施,不创建 worktree/分支:没有额外隔离并发工作区;按任务明确暂存范围避免混入改动。
+- 用 PowerShell 命令和本地执行记录替代 Bash 专用记账脚本:符合 Windows 环境,无产品行为变化,记录与验证命令保持等价。
+- 共有控制器测试集中于 UserOrderNotificationSceneTest:避免重复异步/Spring 夹具,既有三个控制器测试仍运行;仅测试组织不同。
+- 不修基线泰文摊位审核 key 缺失:与本次单字段范围无关,全量回归因此仍红,另行处理。
+- 不纠正旧资料编辑已有“出餐”文字通知:保留用户要求的旧通知行为;其文字仍可能不合理,但不会新增错误语音场景。
+- 云端透传和 App/真机声音交接前端:遵守不改 App/云函数和不部署的边界,本地验证不保证线上投递及播放效果。
 
 ## Dependencies / 前端责任