瀏覽代碼

调整商家小票按订单号查询并返回繁体支付方式

qmj 4 天之前
父節點
當前提交
f2512e5efc

+ 17 - 6
ruoyi-admin/src/main/java/com/ruoyi/app/order/MerchantOrderPrintService.java

@@ -9,7 +9,6 @@ import com.ruoyi.common.utils.MessageUtils;
 import com.ruoyi.common.utils.StringUtils;
 import com.ruoyi.system.domain.PosOrder;
 import com.ruoyi.system.domain.PosStore;
-import com.ruoyi.system.service.IPosOrderService;
 import com.ruoyi.system.service.IPosStoreService;
 import org.springframework.stereotype.Service;
 
@@ -20,16 +19,14 @@ import java.util.List;
 /** 在控制器完成订单归属校验之后,只读装配小票,不执行真实打印或修改订单。 */
 @Service
 public class MerchantOrderPrintService {
-    private final IPosOrderService orders;
     private final IPosStoreService stores;
 
-    public MerchantOrderPrintService(IPosOrderService orders, IPosStoreService stores) {
-        this.orders = orders;
+    public MerchantOrderPrintService(IPosStoreService stores) {
         this.stores = stores;
     }
 
-    public MerchantOrderPrintView getOrderPrintInfo(Long orderId) {
-        PosOrder order = orderId == null ? null : orders.getById(orderId);
+    /** 已按订单号查出的订单直接用于装配,避免权限校验后重复读取。 */
+    public MerchantOrderPrintView getOrderPrintInfo(PosOrder order) {
         if (order == null) throw new ServiceException(MessageUtils.message("merchant.order.not.found"));
         PosStore store = order.getMdId() == null ? null : stores.getById(order.getMdId());
         MerchantOrderPrintView view = new MerchantOrderPrintView();
@@ -45,6 +42,7 @@ public class MerchantOrderPrintService {
         view.setSelfDelivery(order.getSelfDelivery());
         view.setState(order.getState());
         view.setPayType(order.getPayType());
+        view.setPayTypeName(payTypeName(order.getPayType()));
         view.setPayStatus(order.getPayStatus());
         view.setPickUpNum(order.getPickUpNum());
         view.setTableNo(order.getTableNo());
@@ -70,6 +68,19 @@ public class MerchantOrderPrintService {
         return view;
     }
 
+    private String payTypeName(String payType) {
+        if (payType == null) return "未知付款方式";
+        return switch (payType) {
+            case "1" -> "貨到付款";
+            case "2" -> "信用卡";
+            case "3" -> "LINE Pay";
+            case "4" -> "現金";
+            case "5" -> "Apple Pay";
+            case "6" -> "線下轉帳";
+            default -> "未知付款方式";
+        };
+    }
+
     private List<MerchantOrderPrintView.Item> items(String snapshot) {
         try {
             JSONArray rows = JSON.parseArray(snapshot);

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

@@ -106,9 +106,17 @@ public class PosOrderShOprateController extends BaseController {
     @GetMapping("/getOrderPrintInfo")
     @Auth
     @Anonymous
-    public AjaxResult getOrderPrintInfo(@RequestHeader String token, @RequestParam Long orderId) {
-        requireManagedOrder(orderId);
-        return success(merchantOrderPrintService.getOrderPrintInfo(orderId));
+    public AjaxResult getOrderPrintInfo(@RequestHeader String token, @RequestParam String ddId) {
+        if (StringUtils.isBlank(ddId)) {
+            throw new ServiceException(MessageUtils.message("merchant.order.not.found"));
+        }
+        PosOrder order = posOrderService.getOne(new LambdaQueryWrapper<PosOrder>()
+                .eq(PosOrder::getDdId, ddId));
+        if (order == null) {
+            throw new ServiceException(MessageUtils.message("merchant.order.not.found"));
+        }
+        requireManagedOrder(order.getId());
+        return success(merchantOrderPrintService.getOrderPrintInfo(order));
     }
 
 

+ 2 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/order/dto/MerchantOrderPrintView.java

@@ -24,6 +24,8 @@ public class MerchantOrderPrintView {
     private Integer selfDelivery;
     private Long state;
     private String payType;
+    /** 小票直接使用的臺灣繁體付款方式名稱;payType 保留原始碼值。 */
+    private String payTypeName;
     /** 到付订单的1表示付款方式已确认,不表示已收现金。 */
     private Long payStatus;
     private Long pickUpNum;

+ 35 - 12
ruoyi-admin/src/test/java/com/ruoyi/app/order/MerchantOrderPrintControllerTest.java

@@ -26,25 +26,46 @@ class MerchantOrderPrintControllerTest {
         var controller = new PosOrderShOprateController();
         var users = mock(IInfoUserService.class);
         var access = mock(MerchantStoreAccessService.class);
+        var orders = mock(IPosOrderService.class);
         var print = mock(MerchantOrderPrintService.class);
         ReflectionTestUtils.setField(controller, "infoUserService", users);
         ReflectionTestUtils.setField(controller, "merchantStoreAccessService", access);
+        ReflectionTestUtils.setField(controller, "posOrderService", orders);
         ReflectionTestUtils.setField(controller, "merchantOrderPrintService", print);
         InfoUser merchant = new InfoUser();
         merchant.setUserId(10L);
+        PosOrder order = new PosOrder(); order.setId(123L); order.setDdId("ORDER-123");
+        when(orders.getOne(any())).thenReturn(order);
         when(users.getOne(any())).thenReturn(merchant);
         try (var auth = mockStatic(AuthContext.class);
              var messages = mockStatic(MessageUtils.class, call -> call.getArgument(0))) {
             auth.when(AuthContext::requireUserId).thenReturn(10L);
             for (String role : List.of("1", "5")) {
                 merchant.setUserType(role);
-                controller.getOrderPrintInfo("token", 123L);
+                controller.getOrderPrintInfo("token", "ORDER-123");
             }
-            verify(print, times(2)).getOrderPrintInfo(123L);
-            doThrow(new ServiceException("denied")).when(access).requireOrderAccess(10L, 456L);
-            assertThrows(ServiceException.class, () -> controller.getOrderPrintInfo("token", 456L));
-            verify(print, never()).getOrderPrintInfo(456L);
+            verify(print, times(2)).getOrderPrintInfo(order);
+            doThrow(new ServiceException("denied")).when(access).requireOrderAccess(10L, 123L);
+            assertThrows(ServiceException.class, () -> controller.getOrderPrintInfo("token", "ORDER-123"));
+            verify(print, times(2)).getOrderPrintInfo(order);
+        }
+    }
+
+    @Test
+    void blankOrUnknownOrderNumberNeverLoadsReceipt() {
+        var controller = new PosOrderShOprateController();
+        var orders = mock(IPosOrderService.class);
+        var print = mock(MerchantOrderPrintService.class);
+        ReflectionTestUtils.setField(controller, "posOrderService", orders);
+        ReflectionTestUtils.setField(controller, "merchantOrderPrintService", print);
+        try (var messages = mockStatic(MessageUtils.class, call -> call.getArgument(0))) {
+            assertEquals("merchant.order.not.found", assertThrows(ServiceException.class,
+                    () -> controller.getOrderPrintInfo("token", " ")).getMessage());
+            assertEquals("merchant.order.not.found", assertThrows(ServiceException.class,
+                    () -> controller.getOrderPrintInfo("token", "UNKNOWN")).getMessage());
         }
+        verify(orders, times(1)).getOne(any());
+        verifyNoInteractions(print);
     }
 
     @Test
@@ -59,25 +80,27 @@ class MerchantOrderPrintControllerTest {
         ReflectionTestUtils.setField(controller, "linePayOrderGuard",
                 new LinePayOrderGuard(mock(IPosOrderLinePaymentService.class)));
         InfoUser merchant = new InfoUser(); merchant.setUserId(10L);
-        PosOrder order = new PosOrder(); order.setId(123L); order.setShId(10L); order.setMdId(7L);
+        PosOrder order = new PosOrder(); order.setId(123L); order.setDdId("ORDER-123");
+        order.setShId(10L); order.setMdId(7L);
         when(users.getOne(any())).thenReturn(merchant);
+        when(orders.getOne(any())).thenReturn(order);
         when(orders.getById(123L)).thenReturn(order);
         try (var auth = mockStatic(AuthContext.class);
              var messages = mockStatic(MessageUtils.class, call -> call.getArgument(0))) {
             auth.when(AuthContext::requireUserId).thenReturn(10L);
             merchant.setUserType("3");
-            controller.getOrderPrintInfo("token", 123L);
+            controller.getOrderPrintInfo("token", "ORDER-123");
             order.setShId(99L);
-            assertThrows(ServiceException.class, () -> controller.getOrderPrintInfo("token", 123L));
+            assertThrows(ServiceException.class, () -> controller.getOrderPrintInfo("token", "ORDER-123"));
             merchant.setUserType("4"); merchant.setStoreId(7L);
-            controller.getOrderPrintInfo("token", 123L);
+            controller.getOrderPrintInfo("token", "ORDER-123");
             merchant.setStoreId(8L);
-            assertThrows(ServiceException.class, () -> controller.getOrderPrintInfo("token", 123L));
+            assertThrows(ServiceException.class, () -> controller.getOrderPrintInfo("token", "ORDER-123"));
             for (String role : List.of("0", "2")) {
                 merchant.setUserType(role);
-                assertThrows(ServiceException.class, () -> controller.getOrderPrintInfo("token", 123L));
+                assertThrows(ServiceException.class, () -> controller.getOrderPrintInfo("token", "ORDER-123"));
             }
-            verify(print, times(2)).getOrderPrintInfo(123L);
+            verify(print, times(2)).getOrderPrintInfo(order);
         }
     }
 }

+ 24 - 13
ruoyi-admin/src/test/java/com/ruoyi/app/order/MerchantOrderPrintServiceTest.java

@@ -5,7 +5,6 @@ import com.ruoyi.common.exception.ServiceException;
 import com.ruoyi.common.utils.MessageUtils;
 import com.ruoyi.system.domain.PosOrder;
 import com.ruoyi.system.domain.PosStore;
-import com.ruoyi.system.service.IPosOrderService;
 import com.ruoyi.system.service.IPosStoreService;
 import org.junit.jupiter.api.Test;
 
@@ -16,9 +15,8 @@ import static org.junit.jupiter.api.Assertions.*;
 import static org.mockito.Mockito.*;
 
 class MerchantOrderPrintServiceTest {
-    private final IPosOrderService orders = mock(IPosOrderService.class);
     private final IPosStoreService stores = mock(IPosStoreService.class);
-    private final MerchantOrderPrintService service = new MerchantOrderPrintService(orders, stores);
+    private final MerchantOrderPrintService service = new MerchantOrderPrintService(stores);
 
     private PosOrder order() {
         PosOrder order = new PosOrder();
@@ -27,18 +25,31 @@ class MerchantOrderPrintServiceTest {
         order.setFood("[{\"id\":9129,\"name\":\"薑蔥焗三鮮\",\"price\":40,\"otherPrice\":5,\"number\":2,\"ask\":[\"小\",\"中辣\"],\"beizhu\":\"少盐\"}]");
         order.setFoodAmount(90); order.setAmount(100); order.setFreight(40.0);
         order.setShAddress("{\"name\":\"顾客\",\"phone\":\"0900000000\",\"address\":\"台北\",\"addressDetail\":\"二楼\",\"userId\":999}");
-        when(orders.getById(123L)).thenReturn(order);
         PosStore store = new PosStore(); store.setPosName("新店名"); store.setAddress("门店地址"); store.setTelephone("0200000000");
         when(stores.getById(7L)).thenReturn(store);
         return order;
     }
 
+    @Test
+    void returnsTaiwanTraditionalPaymentNameForEachStoredPayType() {
+        PosOrder order = order();
+        String[] names = {"貨到付款", "信用卡", "LINE Pay", "現金", "Apple Pay", "線下轉帳"};
+        for (int type = 1; type <= names.length; type++) {
+            order.setPayType(String.valueOf(type));
+            var view = service.getOrderPrintInfo(order);
+            assertEquals(String.valueOf(type), view.getPayType());
+            assertEquals(names[type - 1], view.getPayTypeName());
+        }
+        order.setPayType("99");
+        assertEquals("未知付款方式", service.getOrderPrintInfo(order).getPayTypeName());
+    }
+
     @Test
     void freezesProductSpecsAndNotesForBothOrdinaryAndMultiStoreChildOrders() throws Exception {
         PosOrder order = order();
         for (String parent : List.of("ORDER-123", "BATCH-123")) {
             order.setParentDdId(parent);
-            var view = service.getOrderPrintInfo(123L);
+            var view = service.getOrderPrintInfo(order);
             assertEquals("下单时店名", view.getStoreName());
             assertEquals("门店地址", view.getStoreAddress());
             assertEquals("ORDER-123", view.getOrderNo());
@@ -63,7 +74,7 @@ class MerchantOrderPrintServiceTest {
         order.setMerchantDeliverySubsidy(new BigDecimal("32"));
         order.setDiscountAmount(1); order.setMdDiscountAmount(2);
         order.setSalesReduction(3); order.setMdSalesReduction(4); order.setPointsReduction(5);
-        var view = service.getOrderPrintInfo(123L);
+        var view = service.getOrderPrintInfo(order);
         assertEquals(100, view.getAmount());
         assertEquals(90, view.getFoodAmount());
         assertEquals(0, view.getFreight().compareTo(new BigDecimal("40")));
@@ -80,7 +91,7 @@ class MerchantOrderPrintServiceTest {
         order.setReservePhone("0900000000"); order.setPickUpTime("18:30");
         for (Long type : List.of(1L, 2L)) {
             order.setType(type);
-            var view = service.getOrderPrintInfo(123L);
+            var view = service.getOrderPrintInfo(order);
             assertNull(view.getRecipient());
             assertEquals(8L, view.getPickUpNum());
             assertEquals("A3", view.getTableNo());
@@ -91,26 +102,26 @@ class MerchantOrderPrintServiceTest {
             assertTrue(view.getItems().getFirst().specifications().isEmpty());
         }
         order.setShAddress("null");
-        assertNull(service.getOrderPrintInfo(123L).getRecipient());
+        assertNull(service.getOrderPrintInfo(order).getRecipient());
     }
 
     @Test
     void missingStoreUsesStoredNameAndMissingOrderOrCorruptSnapshotFailsExplicitly() {
         PosOrder order = order();
         when(stores.getById(7L)).thenReturn(null);
-        assertEquals("下单时店名", service.getOrderPrintInfo(123L).getStoreName());
-        assertNull(service.getOrderPrintInfo(123L).getStoreAddress());
+        assertEquals("下单时店名", service.getOrderPrintInfo(order).getStoreName());
+        assertNull(service.getOrderPrintInfo(order).getStoreAddress());
         try (var messages = mockStatic(MessageUtils.class, call -> call.getArgument(0))) {
             assertEquals("merchant.order.not.found", assertThrows(ServiceException.class,
-                    () -> service.getOrderPrintInfo(999L)).getMessage());
+                    () -> service.getOrderPrintInfo(null)).getMessage());
             for (String data : List.of("bad-json", "[]", "[null]", "[{\"name\":\"商品\",\"number\":0}]")) {
                 order.setFood(data);
                 assertEquals("merchant.order.print.invalidSnapshot", assertThrows(ServiceException.class,
-                        () -> service.getOrderPrintInfo(123L)).getMessage());
+                        () -> service.getOrderPrintInfo(order)).getMessage());
             }
             order.setFood("[{\"name\":\"商品\",\"number\":1}]"); order.setShAddress("bad-address");
             assertEquals("merchant.order.print.invalidSnapshot", assertThrows(ServiceException.class,
-                    () -> service.getOrderPrintInfo(123L)).getMessage());
+                    () -> service.getOrderPrintInfo(order)).getMessage());
         }
     }
 }

+ 5 - 3
specs/039-merchant-order-receipt/frontend-app-integration.md

@@ -6,9 +6,9 @@
 
 小票功能直接提供,无需查询启用状态或配置字典。
 
-`GET /system/orderShOprate/getOrderPrintInfo?orderId=3173`
+`GET /system/orderShOprate/getOrderPrintInfo?ddId=991789716730512`
 
-`orderId` 是订单列表里的 `pos_order.id`,**不是**订单号 `ddId`、父单号或多店批次 ID。多店订单对每张子单分别读取,按对应门店打印。
+`ddId` 是订单列表里的完整订单号 `pos_order.dd_id`,作为字符串传入,**不是**数据库主键 `orderId`、父单号或多店批次 ID。多店订单对每张子单的 `ddId` 分别读取,按对应门店打印。
 
 ### 返回字段(data)
 
@@ -21,7 +21,7 @@
 | orderType | long | 0外送、1自取、2堂食 |
 | selfDelivery | int | 1商家自配送、0平台配送;老单可能为空 |
 | state | long | 订单现有状态码,沿用商家订单页面解释 |
-| payType / payStatus | string / long | 原订单支付方式 / 支付状态,不做文字转换 |
+| payType / payTypeName / payStatus | string / string / long | 原訂單支付方式代碼 / 臺灣繁體付款方式名稱 / 原訂單支付狀態 |
 | pickUpNum / tableNo | long / string | 取餐号 / 桌号;不适用时为空 |
 | delryTime / pickUpTime | string | 原订单预约配送信息 / 自取时间,原样展示;可能为空 |
 | reservePhone | string | 自取预留电话;可能为空 |
@@ -59,6 +59,8 @@
 
 ### 关键处理
 
+`payTypeName` 由後端按 `payType` 返回:`1` 貨到付款、`2` 信用卡、`3` LINE Pay、`4` 現金、`5` Apple Pay、`6` 線下轉帳;空值或未知代碼返回「未知付款方式」。App 小票可直接顯示此欄,仍應以 `payType` 判斷付款流程。
+
 - 收货人、商品、规格、备注、金额取下单快照,不因用户改地址、商品改价或规格删除而改变。
 - 到付 `payType="1"` 即使 `payStatus=1`,也不代表客户已经交现金,应标记到付,不能标记已收款。
 - 用户、骑手和无权限的商家不能读取;分管账号受授权门店限制,摊主受绑定摊位限制。

+ 8 - 0
specs/039-merchant-order-receipt/plan.md

@@ -8,3 +8,11 @@
 4. 更新 App 接入文档,执行 JDK21 定向测试及模块打包、差异检查。设备绑定、自动打印和真机出纸不在本轮验证范围。
 
 备选:直接返回 PosOrder 会暴露无关信息并要求 App 解析 JSON;复刻 CTE 的 Map 会丢失本系统运费契约。采用明确 DTO 和快照转换,不引入新表或新的订单状态。
+
+## 2026-10-29 查询参数调整
+
+前端改传完整订单号 `ddId`;控制器先按 `pos_order.dd_id` 查询、再用所得订单主键执行现有商家权限检查,服务层复用已查出的订单快照装配小票。响应字段保持不变。
+
+## 2026-10-30 支付方式文字
+
+小票响应保留支付方式原始码值,新增 `payTypeName`,根据订单已保存的 `payType` 映射为台湾繁体名称。未知或空码值给出「未知付款方式」,不改变支付流程。

+ 2 - 2
specs/039-merchant-order-receipt/spec.md

@@ -3,7 +3,7 @@
 ## 已确认范围(2026-10-29)
 
 小票 GET 接口放在现有商家订单控制器 `/system/orderShOprate`:
-- `/getOrderPrintInfo?orderId=...`:按本系统字段提供一张真实订单的小票数据。
+- `/getOrderPrintInfo?ddId=...`:按本系统字段提供一张真实订单的小票数据。
 
 只做后端和 App 接入说明;不修改 App、不绑定或驱动打印机、不发送打印任务,不改变普通单、多店子单的付款或履约流程。不新增分支。
 
@@ -13,7 +13,7 @@
 - 小票功能直接提供,不设置启用开关,不增加字典或初始化 SQL。
 - 商品从 `pos_order.food` 快照解析成数组,包含名称、数量、价格、规格加价、已选规格和逐商品备注;不查当前商品或规格,不重新计算订单金额。
 - 订单金额、优惠及运费均来自订单快照。区分骑手原运费 `freight` 和客户实付运费 `customerDeliveryFee`;旧订单无新字段时,实付运费回退 freight。优惠分项和合计不包含商家配送补贴,避免混算。
-- 订单类型、自配送、支付方式及支付状态返回码值,由 App 本地化;到付 payStatus=1 不代表已收到现金,不能据此打印“已收款”。
+- 订单类型、自配送及支付状态返回码值;支付方式同时返回原始 `payType` 和台湾繁体 `payTypeName`,供 App 小票直接展示。到付 payStatus=1 不代表已收到现金,不能据此打印“已收款”。
 - 收货信息只读订单地址快照;自取/堂食无收货人正常返回 null,并保留取餐号、预留电话、桌号及预约信息。
 - 商品 JSON 损坏时明确返回国际化错误,不伪造空白小票;缺失门店时保留订单店名快照,门店地址/电话为空。不返回用户整表或支付密钥。
 

+ 12 - 0
specs/039-merchant-order-receipt/tasks.md

@@ -16,3 +16,15 @@
 - 提交前再次运行同范围 JDK21 `package` 成功:27项0失败/错误/跳过,已生成最新JAR,日志 `ruoyi-admin/target/merchant-print-commit.log`。此条更新上一条未重新打包的历史状态。
 - 未执行真实数据库联调、App/打印设备联调、实际出纸或部署。本轮无需数据库结构调整、字典配置或 SQL。
 - 自动打印触发、防重复、设备绑定、重试与补打记录均未实现;本轮只交付用户明确要求的小票数据接口。
+
+## 2026-10-29 ddId 查询调整
+
+- [x] GET 参数改为 `ddId`,按订单号查单并继续使用原有商家权限校验。
+- [x] 更新控制器测试与前端接入文档,响应不变。
+- [x] 完成定向测试与模块构建:JDK21 定向运行 11 项测试,0 失败/错误/跳过;`ruoyi-admin` 模块打包成功。
+
+## 2026-10-30 支付方式文字
+
+- [x] 小票保留 `payType`,新增台湾繁体 `payTypeName`,覆盖现有六种码值及未知码值。
+- [x] 更新 App 接入说明与规格。
+- [x] JDK21 定向运行 12 项测试,0 失败/错误/跳过;`ruoyi-admin` 模块打包成功。