Browse Source

限制订单退款操作到授权门店

qmj 1 day ago
parent
commit
4403defd6b

+ 63 - 8
ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderController.java

@@ -21,6 +21,7 @@ import com.ruoyi.app.pay.LinePayRefundService;
 import com.ruoyi.app.order.dto.AdminOrderStatusUpdateRequest;
 import com.ruoyi.app.service.UserService;
 import com.ruoyi.app.service.WalletService;
+import com.ruoyi.app.user.MerchantTokenSessionService;
 import com.ruoyi.app.user.dto.StoreOutput;
 import com.ruoyi.app.utils.DateUtil;
 import com.ruoyi.app.utils.OperatingUtil;
@@ -164,6 +165,10 @@ public class PosOrderController extends BaseController {
     private IPosOrderLinePaymentService linePaymentService;
     @Autowired
     private IOmgPaymentAttemptService omgPaymentAttemptService;
+    @Autowired
+    private MerchantStoreAccessService merchantStoreAccessService;
+    @Autowired
+    private MerchantTokenSessionService merchantTokenSessionService;
 
 
     //查询用户足迹
@@ -228,10 +233,17 @@ public class PosOrderController extends BaseController {
     @PostMapping("/setorderuzt")
     @Transactional(rollbackFor = Exception.class)
     public AjaxResult setorderuzt(@RequestHeader String token, @RequestBody PosOrder posOrder) {
-        JwtUtil jwtUtil = new JwtUtil();
         PayPush push = new PayPush();
-        String id = jwtUtil.getusid(token);
+        Long loginUserId = merchantTokenSessionService.requireUserId(token);
+        String id = String.valueOf(loginUserId);
         PosOrder persisted = posOrder.getId() == null ? null : posOrderService.getById(posOrder.getId());
+        InfoUser actor = infoUserService.getById(loginUserId);
+        if (actor != null && ("1".equals(actor.getUserType()) || "5".equals(actor.getUserType()))) {
+            merchantStoreAccessService.requireOrderAccess(loginUserId,
+                    persisted == null ? null : persisted.getId());
+        } else {
+            linePayOrderGuard.requireLegacyOrderActor(persisted, actor, posOrder.getState());
+        }
         boolean actualLineOrder = persisted != null && linePaymentService != null
                 && !linePaymentService.getByDdId(String.valueOf(persisted.getDdId())).isEmpty();
         if (actualLineOrder) {
@@ -691,10 +703,11 @@ public class PosOrderController extends BaseController {
     @Anonymous
     @Auth
     @GetMapping("/getorderxq")
-    public AjaxResult getorderxq(@RequestParam String ddid) {
+    public AjaxResult getorderxq(@RequestHeader String token, @RequestParam String ddid) {
         QueryWrapper<PosOrder> queryWrapper = new QueryWrapper<>();
         queryWrapper.eq("dd_id", ddid);
         PosOrder orlist = posOrderService.getOne(queryWrapper);
+        requireOrderDetailAccess(token, orlist);
         // 自动序列化所有PosOrder字段,新增字段无需手动添加
         JSONObject org = JSON.parseObject(
             JSON.toJSONStringWithDateFormat(orlist, "yyyy-MM-dd HH:mm:ss")
@@ -719,6 +732,31 @@ public class PosOrderController extends BaseController {
         return success(org);
     }
 
+    private void requireOrderDetailAccess(String token, PosOrder order) {
+        Long loginUserId = merchantTokenSessionService.requireUserId(token);
+        InfoUser actor = infoUserService.getById(loginUserId);
+        if (order == null || actor == null) {
+            throw new ServiceException(MessageUtils.message("merchant.order.not.found"));
+        }
+        if ("0".equals(actor.getUserType())) {
+            if (!loginUserId.equals(order.getUserId())) {
+                throw new ServiceException(MessageUtils.message("merchant.order.not.found"));
+            }
+            return;
+        }
+        if ("2".equals(actor.getUserType())) {
+            if (!loginUserId.equals(order.getQsId())) {
+                throw new ServiceException(MessageUtils.message("merchant.order.not.found"));
+            }
+            return;
+        }
+        if ("1".equals(actor.getUserType()) || "5".equals(actor.getUserType())) {
+            merchantStoreAccessService.requireOrderAccess(loginUserId, order.getId());
+            return;
+        }
+        linePayOrderGuard.requireMerchantOwnership(order, actor);
+    }
+
     /**
      * 设置骑手星级
      *
@@ -845,14 +883,31 @@ public class PosOrderController extends BaseController {
     @Auth
     @GetMapping("/getstoreorderlist")
     public AjaxResult getstoreorderlist(@RequestHeader String token, @RequestParam Integer page, @RequestParam Integer size, @RequestParam String mdId, @RequestParam(defaultValue = "") String state, @RequestParam(defaultValue = "") String diningStatus,@RequestParam(defaultValue = "")String type) {
-        JwtUtil jwtUtil = new JwtUtil();
-        String id = jwtUtil.getusid(token);
+        Long loginUserId = merchantTokenSessionService.requireUserId(token);
+        Long requestedStoreId;
+        try {
+            requestedStoreId = Long.valueOf(mdId);
+        } catch (NumberFormatException exception) {
+            throw new ServiceException(MessageUtils.message("merchant.store.access.denied"));
+        }
+        InfoUser actor = infoUserService.getById(loginUserId);
+        if (actor == null) {
+            throw new ServiceException(MessageUtils.message("merchant.account.unavailable"));
+        }
         IPage<PosOrder> palist = new Page<>(page, size);
         QueryWrapper<PosOrder> queryWrapper = new QueryWrapper<>();
         queryWrapper.select().orderByDesc("cretim");
-        queryWrapper.eq("sh_id", id);
-        System.out.println("商户id:" + id);
-        queryWrapper.eq("md_id", mdId).eq("is_display", true);
+        if ("1".equals(actor.getUserType()) || "5".equals(actor.getUserType())) {
+            merchantStoreAccessService.requireStoreAccess(loginUserId, requestedStoreId);
+            queryWrapper.eq("md_id", requestedStoreId);
+        } else if ("3".equals(actor.getUserType())) {
+            queryWrapper.eq("sh_id", loginUserId).eq("md_id", requestedStoreId);
+        } else if (actor.getStoreId() != null && actor.getStoreId().equals(requestedStoreId)) {
+            queryWrapper.eq("md_id", actor.getStoreId());
+        } else {
+            throw new ServiceException(MessageUtils.message("merchant.store.access.denied"));
+        }
+        queryWrapper.eq("is_display", true);
         queryWrapper.eq(!StringUtils.isEmpty(type),"type",type);
         if (!"".equals(state)) {
             if (state.equals("z01")) {

+ 85 - 27
ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderShOprateController.java

@@ -12,6 +12,7 @@ import com.ruoyi.app.order.dto.OrderCreateInput;
 import com.ruoyi.app.order.dto.OrderPushBodyDto;
 import com.ruoyi.app.pay.LinePayOrderGuard;
 import com.ruoyi.app.pay.LinePayRefundService;
+import com.ruoyi.app.user.MerchantTokenSessionService;
 import com.ruoyi.app.utils.PayPush;
 import com.ruoyi.app.utils.event.PushEventService;
 import com.ruoyi.common.annotation.Anonymous;
@@ -72,6 +73,10 @@ public class PosOrderShOprateController extends BaseController {
     private OrderLifecycleService orderLifecycleService;
     @Autowired
     private DeliveryOrderNotificationService deliveryOrderNotificationService;
+    @Autowired
+    private MerchantStoreAccessService merchantStoreAccessService;
+    @Autowired
+    private MerchantTokenSessionService merchantTokenSessionService;
 
 
 
@@ -83,8 +88,8 @@ public class PosOrderShOprateController extends BaseController {
     @Auth
     @Anonymous
     public AjaxResult getInvoice(@RequestHeader String token, @RequestParam Long orderId) {
-        Long userId = Long.valueOf(new JwtUtil().getusid(token));
-        return AjaxResult.success(orderInvoiceService.getInvoiceForMerchant(orderId, userId));
+        MerchantAccessContext context = requireManagedOrder(token, orderId);
+        return AjaxResult.success(orderInvoiceService.getInvoiceForMerchant(orderId, context.ownerUserId()));
     }
 
     /**
@@ -96,9 +101,9 @@ public class PosOrderShOprateController extends BaseController {
     @Anonymous
     public AjaxResult invalidInvoice(@RequestHeader String token, @PathVariable Long orderId,
                                      @RequestBody(required = false) InvalidInvoiceDto dto) {
-        Long userId = Long.valueOf(new JwtUtil().getusid(token));
+        MerchantAccessContext context = requireManagedOrder(token, orderId);
         String reason = dto == null ? null : dto.getInvalidReason();
-        return toAjax(orderInvoiceService.invalidForMerchant(orderId, reason, userId));
+        return toAjax(orderInvoiceService.invalidForMerchant(orderId, reason, context.ownerUserId()));
     }
 
     /**
@@ -110,14 +115,14 @@ public class PosOrderShOprateController extends BaseController {
     @Transactional(rollbackFor = Exception.class)
     public AjaxResult createOrder(@RequestHeader String token, @RequestBody OrderCreateInput input)
     {
-        JwtUtil jwtUtil = new JwtUtil();
-        String id = jwtUtil.getusid(token);
+        Long loginUserId = merchantTokenSessionService.requireUserId(token);
         InfoUser merchant = infoUserService.getOne(new LambdaQueryWrapper<InfoUser>()
-                .eq(InfoUser::getUserId, Long.valueOf(id)));
+                .eq(InfoUser::getUserId, loginUserId));
         validateMerchantOrderInput(merchant, input);
+        Long ownerUserId = resolveOrderOwner(merchant);
         String shdz=null;
-        createOrderParent(input,Long.valueOf(id),shdz);
-        createOrderChild(input,Long.valueOf(id),Long.valueOf(id),shdz);
+        createOrderParent(input,ownerUserId,shdz);
+        createOrderChild(input,ownerUserId,ownerUserId,shdz);
 
         OrderParent result = orderParentService.getOne(new LambdaQueryWrapper<OrderParent>().eq(OrderParent::getDdId, input.getDdId().toString()));
         System.out.println("创建返回信息:"+result);
@@ -231,6 +236,12 @@ public class PosOrderShOprateController extends BaseController {
                 || item.getAmount() <= 0) {
             throw new ServiceException(MessageUtils.message("line.pay.offline.order.invalid"));
         }
+        if ("5".equals(merchant.getUserType())) {
+            MerchantAccessContext context = merchantStoreAccessService.resolve(merchant.getUserId());
+            merchantStoreAccessService.requireStoreAccess(merchant.getUserId(), item.getMdId());
+            item.setShId(context.ownerUserId());
+            return;
+        }
         boolean directMerchant = "1".equals(merchant.getUserType())
                 || "3".equals(merchant.getUserType());
         PosStore store = directMerchant ? posStoreService.getById(item.getMdId()) : null;
@@ -243,6 +254,13 @@ public class PosOrderShOprateController extends BaseController {
         }
     }
 
+    private Long resolveOrderOwner(InfoUser merchant) {
+        if (merchant != null && "5".equals(merchant.getUserType())) {
+            return merchantStoreAccessService.resolve(merchant.getUserId()).ownerUserId();
+        }
+        return merchant == null ? null : merchant.getUserId();
+    }
+
     //生成取餐码,根据门店id,当天取餐码从1开始累加
     private void setPickUpNum(PosOrder order) {
         if (order == null || order.getMdId() == null) {
@@ -287,11 +305,10 @@ public class PosOrderShOprateController extends BaseController {
     @GetMapping("/acceptOrder")
     public AjaxResult acceptOrder(@RequestHeader String token, @RequestParam Long id) {
         PosOrder order = posOrderService.getOne(new LambdaQueryWrapper<PosOrder>().eq(PosOrder::getId, id));
+        InfoUser currentUser = requireMerchantOrderAccess(token, order);
         if (order == null || order.getState() == null || order.getState() != 0L) {
             throw new ServiceException("订单不存在或状态不允许接单");
         }
-        InfoUser currentUser = currentMerchant(token);
-        linePayOrderGuard.requireMerchantOwnership(order, currentUser);
         linePayOrderGuard.requirePaidBeforeAccept(order);
         requireRiderAssigned(order);
         PosOrder update = new PosOrder();
@@ -313,10 +330,10 @@ public class PosOrderShOprateController extends BaseController {
     @GetMapping("/dispatchOrder")
     public AjaxResult dispatchOrder(@RequestHeader String token, @RequestParam Long id) {
         PosOrder order = posOrderService.getOne(new LambdaQueryWrapper<PosOrder>().eq(PosOrder::getId, id));
+        requireMerchantOrderAccess(token, order);
         if (order == null || order.getState() == null || order.getState() != 1L) {
             throw new ServiceException("订单不存在或状态不允许出餐");
         }
-        linePayOrderGuard.requireMerchantOwnership(order, currentMerchant(token));
         linePayOrderGuard.requirePaidBeforeAccept(order);
         requireRiderAssigned(order);
         PosOrder update = new PosOrder();
@@ -345,10 +362,10 @@ public class PosOrderShOprateController extends BaseController {
     @GetMapping("/completeOrder")
     public AjaxResult completeOrder(@RequestHeader String token, @RequestParam Long id) {
         PosOrder order = posOrderService.getOne(new LambdaQueryWrapper<PosOrder>().eq(PosOrder::getId, id));
+        requireMerchantOrderAccess(token, order);
         if (order == null || order.getState() == null || order.getState() != 2L) {
             throw new ServiceException("订单不存在或状态不允许完成");
         }
-        linePayOrderGuard.requireMerchantOwnership(order, currentMerchant(token));
         linePayOrderGuard.requirePaidBeforeAccept(order);
         // 仅限自取(type=1)或堂食(type=2)
         if (order.getType() == null || (order.getType() != 1L && order.getType() != 2L)) {
@@ -382,10 +399,10 @@ public class PosOrderShOprateController extends BaseController {
     @GetMapping("/confirmCashPayment")
     public AjaxResult confirmCashPayment(@RequestHeader String token, @RequestParam Long id) {
         PosOrder order = posOrderService.getOne(new LambdaQueryWrapper<PosOrder>().eq(PosOrder::getId, id));
+        requireMerchantOrderAccess(token, order);
         if (order == null) {
             throw new ServiceException(MessageUtils.message("no.order.not.exist"));
         }
-        linePayOrderGuard.requireMerchantOwnership(order, currentMerchant(token));
         if (!OrderLifecycleService.PAY_TYPE_CASH.equals(order.getPayType())) {
             throw new ServiceException(MessageUtils.message("no.order.paytype.not.cash"));
         }
@@ -414,11 +431,10 @@ public class PosOrderShOprateController extends BaseController {
     @GetMapping("/cancelOrder")
     public AjaxResult cancelOrder(@RequestHeader String token, @RequestParam Long id) {
         PosOrder order = posOrderService.getOne(new LambdaQueryWrapper<PosOrder>().eq(PosOrder::getId, id));
+        InfoUser currentUser = requireMerchantOrderAccess(token, order);
         if (order == null || order.getState() == null) {
             throw new ServiceException("订单不存在");
         }
-        InfoUser currentUser = currentMerchant(token);
-        linePayOrderGuard.requireMerchantOwnership(order, currentUser);
         if (order.getState() != 0L && order.getState() != 1L) {
             throw new ServiceException("当前状态不允许取消");
         }
@@ -449,7 +465,7 @@ public class PosOrderShOprateController extends BaseController {
     }
 
     private InfoUser currentMerchant(String token) {
-        Long userId = Long.valueOf(new JwtUtil().getusid(token));
+        Long userId = merchantTokenSessionService.requireUserId(token);
         InfoUser user = infoUserService.getOne(new LambdaQueryWrapper<InfoUser>()
                 .eq(InfoUser::getUserId, userId));
         if (user == null) {
@@ -458,6 +474,31 @@ public class PosOrderShOprateController extends BaseController {
         return user;
     }
 
+    private InfoUser requireMerchantOrderAccess(String token, PosOrder order) {
+        InfoUser user = currentMerchant(token);
+        if ("1".equals(user.getUserType()) || "5".equals(user.getUserType())) {
+            merchantStoreAccessService.requireOrderAccess(user.getUserId(), order == null ? null : order.getId());
+        } else {
+            linePayOrderGuard.requireMerchantOwnership(order, user);
+        }
+        return user;
+    }
+
+    private MerchantAccessContext requireManagedOrder(String token, Long orderId) {
+        Long loginUserId = merchantTokenSessionService.requireUserId(token);
+        InfoUser user = infoUserService.getOne(new LambdaQueryWrapper<InfoUser>()
+                .eq(InfoUser::getUserId, loginUserId));
+        if (user != null && ("1".equals(user.getUserType()) || "5".equals(user.getUserType()))) {
+            MerchantAccessContext context = merchantStoreAccessService.resolve(loginUserId);
+            merchantStoreAccessService.requireOrderAccess(loginUserId, orderId);
+            return context;
+        }
+        PosOrder order = posOrderService.getById(orderId);
+        linePayOrderGuard.requireMerchantOwnership(order, user);
+        return new MerchantAccessContext(loginUserId, loginUserId, true,
+                order == null || order.getMdId() == null ? Set.of() : Set.of(order.getMdId()));
+    }
+
     private void requestLineRefundIfPaid(PosOrder order, String source) {
         if (order == null || !Long.valueOf(1L).equals(order.getPayStatus())) {
             return;
@@ -529,23 +570,28 @@ public class PosOrderShOprateController extends BaseController {
                                 @RequestParam(required = false) String tab,
                                 @RequestParam(required = false) Long mdId,
                                 @RequestParam(required = false) String type) {
-        JwtUtil jwtUtil = new JwtUtil();
-        String userId = jwtUtil.getusid(token);
+        Long loginUserId = merchantTokenSessionService.requireUserId(token);
         // 查询当前用户信息
-        InfoUser currentUser = infoUserService.getOne(new LambdaQueryWrapper<InfoUser>().eq(InfoUser::getUserId, Long.valueOf(userId)));
+        InfoUser currentUser = infoUserService.getOne(new LambdaQueryWrapper<InfoUser>().eq(InfoUser::getUserId, loginUserId));
         if (currentUser == null) {
             throw new ServiceException("用户信息不存在");
         }
 
         LambdaQueryWrapper<PosOrder> wrapper = new LambdaQueryWrapper<>();
-        wrapper.eq(mdId!=null,PosOrder::getMdId,mdId);
         String userType = currentUser.getUserType();
-        if ("3".equals(userType)) {
+        if ("1".equals(userType) || "5".equals(userType)) {
+            Set<Long> storeIds = merchantStoreAccessService.getAccessibleStoreIds(loginUserId);
+            if (mdId != null) {
+                merchantStoreAccessService.requireStoreAccess(loginUserId, mdId);
+                wrapper.eq(PosOrder::getMdId, mdId);
+            } else if (storeIds.isEmpty()) {
+                wrapper.apply("1 = 0");
+            } else {
+                wrapper.in(PosOrder::getMdId, storeIds);
+            }
+        } else if ("3".equals(userType)) {
             // 夜市商家:shId等于用户id
-            wrapper.eq(PosOrder::getShId, Long.valueOf(userId));
-        } else if ("1".equals(userType)) {
-            // 普通商家:shId等于用户id
-            wrapper.eq(PosOrder::getShId, Long.valueOf(userId));
+            wrapper.eq(PosOrder::getShId, loginUserId).eq(mdId != null, PosOrder::getMdId, mdId);
         } else {
             // 摊位商家:mdId等于用户表中的storeId
             if (currentUser.getStoreId() == null) {
@@ -556,7 +602,7 @@ public class PosOrderShOprateController extends BaseController {
         if (type != null && !type.isEmpty()) {
             wrapper.eq(PosOrder::getType, Long.valueOf(type));
         }
-        switch (tab) {
+        switch (tab == null ? "" : tab) {
             case "pending":
                 wrapper.eq(PosOrder::getState, 0L)
                        // 现金自取/堂食可直接待受理;在线支付的自取/堂食必须先完成付款。
@@ -591,4 +637,16 @@ public class PosOrderShOprateController extends BaseController {
         return success(result);
     }
 
+    @Anonymous
+    @Auth
+    @GetMapping("/orderDetail")
+    public AjaxResult orderDetail(@RequestHeader String token, @RequestParam Long id) {
+        requireManagedOrder(token, id);
+        PosOrder order = posOrderService.getById(id);
+        if (order == null) {
+            throw new ServiceException(MessageUtils.message("merchant.order.not.found"));
+        }
+        return success(order);
+    }
+
 }

+ 55 - 0
ruoyi-admin/src/test/java/com/ruoyi/app/order/OrderListPaymentFilterTest.java

@@ -9,26 +9,36 @@ import com.ruoyi.system.domain.InfoUser;
 import com.ruoyi.system.domain.PosOrder;
 import com.ruoyi.system.service.IInfoUserService;
 import com.ruoyi.system.service.IPosOrderService;
+import com.ruoyi.system.service.MerchantStoreAccessService;
+import com.ruoyi.app.user.MerchantTokenSessionService;
 import com.ruoyi.system.utils.JwtUtil;
+import com.ruoyi.common.utils.MessageUtils;
+import org.junit.jupiter.api.AfterEach;
 import org.apache.ibatis.builder.MapperBuilderAssistant;
 import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
 import org.mockito.ArgumentCaptor;
 import org.springframework.test.util.ReflectionTestUtils;
 
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Set;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 class OrderListPaymentFilterTest {
+    private MockedStatic<MessageUtils> messages;
 
     @BeforeAll
     static void initializeTableMetadata() {
@@ -79,6 +89,12 @@ class OrderListPaymentFilterTest {
         PosOrderShOprateController controller = new PosOrderShOprateController();
         ReflectionTestUtils.setField(controller, "posOrderService", orders);
         ReflectionTestUtils.setField(controller, "infoUserService", users);
+        MerchantTokenSessionService sessions = mock(MerchantTokenSessionService.class);
+        MerchantStoreAccessService access = mock(MerchantStoreAccessService.class);
+        when(sessions.requireUserId(any())).thenReturn(801L);
+        when(access.getAccessibleStoreIds(801L)).thenReturn(Set.of(11L, 12L));
+        ReflectionTestUtils.setField(controller, "merchantTokenSessionService", sessions);
+        ReflectionTestUtils.setField(controller, "merchantStoreAccessService", access);
         InfoUser merchant = new InfoUser();
         merchant.setUserId(801L);
         merchant.setUserType("1");
@@ -96,6 +112,45 @@ class OrderListPaymentFilterTest {
         assertEquals(1, Collections.frequency(parameters, "1"));
     }
 
+    @BeforeEach
+    void setUpMessages() {
+        messages = mockStatic(MessageUtils.class);
+        messages.when(() -> MessageUtils.message(anyString()))
+                .thenAnswer(invocation -> invocation.getArgument(0));
+    }
+
+    @AfterEach
+    void closeMessages() {
+        messages.close();
+    }
+
+    @Test
+    void subaccountOrderListFiltersByAllAuthorizedStores() {
+        IPosOrderService orders = mock(IPosOrderService.class);
+        IInfoUserService users = mock(IInfoUserService.class);
+        MerchantTokenSessionService sessions = mock(MerchantTokenSessionService.class);
+        MerchantStoreAccessService access = mock(MerchantStoreAccessService.class);
+        PosOrderShOprateController controller = new PosOrderShOprateController();
+        ReflectionTestUtils.setField(controller, "posOrderService", orders);
+        ReflectionTestUtils.setField(controller, "infoUserService", users);
+        ReflectionTestUtils.setField(controller, "merchantTokenSessionService", sessions);
+        ReflectionTestUtils.setField(controller, "merchantStoreAccessService", access);
+        InfoUser subaccount = new InfoUser();
+        subaccount.setUserId(501L);
+        subaccount.setUserType("5");
+        when(sessions.requireUserId(any())).thenReturn(501L);
+        when(users.getOne(any())).thenReturn(subaccount);
+        when(access.getAccessibleStoreIds(501L)).thenReturn(Set.of(11L, 12L));
+        when(orders.page(any(Page.class), any(Wrapper.class))).thenReturn(new Page<>());
+
+        controller.orderList(tokenFor(501L), 1, 10, "", null, null);
+
+        LambdaQueryWrapper<PosOrder> query = capturedQuery(orders);
+        assertTrue(query.getSqlSegment().contains("md_id IN"));
+        assertTrue(query.getParamNameValuePairs().containsValue(11L));
+        assertTrue(query.getParamNameValuePairs().containsValue(12L));
+    }
+
     @SuppressWarnings({"rawtypes", "unchecked"})
     private static LambdaQueryWrapper<PosOrder> capturedQuery(IPosOrderService orders) {
         ArgumentCaptor<Wrapper> captor = ArgumentCaptor.forClass(Wrapper.class);

+ 14 - 0
ruoyi-admin/src/test/java/com/ruoyi/app/order/PosOrderAdminStatusControllerTest.java

@@ -2,12 +2,16 @@ package com.ruoyi.app.order;
 
 import com.ruoyi.app.order.dto.AdminOrderActionRequest;
 import com.ruoyi.app.order.dto.AdminOrderStatusUpdateRequest;
+import com.ruoyi.app.user.MerchantTokenSessionService;
 import com.ruoyi.common.core.domain.AjaxResult;
 import com.ruoyi.common.exception.ServiceException;
 import com.ruoyi.system.domain.PosOrder;
 import com.ruoyi.system.domain.PosOrderLinePayment;
+import com.ruoyi.system.domain.InfoUser;
+import com.ruoyi.system.service.IInfoUserService;
 import com.ruoyi.system.service.IPosOrderLinePaymentService;
 import com.ruoyi.system.service.IPosOrderService;
+import com.ruoyi.system.service.MerchantStoreAccessService;
 import com.ruoyi.system.utils.JwtUtil;
 import org.junit.jupiter.api.Test;
 import org.springframework.security.access.prepost.PreAuthorize;
@@ -70,6 +74,11 @@ class PosOrderAdminStatusControllerTest {
         IPosOrderLinePaymentService paymentService = mock(IPosOrderLinePaymentService.class);
         ReflectionTestUtils.setField(controller, "posOrderService", orderService);
         ReflectionTestUtils.setField(controller, "linePaymentService", paymentService);
+        MerchantTokenSessionService sessions = mock(MerchantTokenSessionService.class);
+        IInfoUserService users = mock(IInfoUserService.class);
+        ReflectionTestUtils.setField(controller, "merchantTokenSessionService", sessions);
+        ReflectionTestUtils.setField(controller, "merchantStoreAccessService", mock(MerchantStoreAccessService.class));
+        ReflectionTestUtils.setField(controller, "infoUserService", users);
         PosOrder stored = new PosOrder();
         stored.setId(1L);
         stored.setDdId("D-1");
@@ -77,6 +86,11 @@ class PosOrderAdminStatusControllerTest {
         PosOrderLinePayment payment = new PosOrderLinePayment();
         payment.setDdId("D-1");
         when(orderService.getById(1L)).thenReturn(stored);
+        when(sessions.requireUserId(org.mockito.ArgumentMatchers.anyString())).thenReturn(9L);
+        InfoUser merchant = new InfoUser();
+        merchant.setUserId(9L);
+        merchant.setUserType("1");
+        when(users.getById(9L)).thenReturn(merchant);
         when(paymentService.getByDdId("D-1")).thenReturn(java.util.List.of(payment));
         PosOrder malicious = new PosOrder();
         malicious.setId(1L);

+ 96 - 0
ruoyi-admin/src/test/java/com/ruoyi/app/order/PosOrderShOprateControllerTest.java

@@ -3,6 +3,8 @@ package com.ruoyi.app.order;
 import com.ruoyi.app.order.dto.OrderCreatItem;
 import com.ruoyi.app.order.dto.OrderCreateInput;
 import com.ruoyi.app.pay.LinePayOrderGuard;
+import com.ruoyi.app.pay.LinePayRefundService;
+import com.ruoyi.app.user.MerchantTokenSessionService;
 import com.ruoyi.common.exception.ServiceException;
 import com.ruoyi.common.utils.MessageUtils;
 import com.ruoyi.system.domain.InfoUser;
@@ -14,6 +16,8 @@ import com.ruoyi.system.service.IOperatingHoursService;
 import com.ruoyi.system.service.IOrderParentService;
 import com.ruoyi.system.service.IPosOrderService;
 import com.ruoyi.system.service.IPosStoreService;
+import com.ruoyi.system.service.MerchantAccessContext;
+import com.ruoyi.system.service.MerchantStoreAccessService;
 import com.ruoyi.system.utils.JwtUtil;
 import com.ruoyi.system.utils.OrderLogHelper;
 import org.junit.jupiter.api.AfterEach;
@@ -24,6 +28,7 @@ import org.mockito.ArgumentCaptor;
 import org.springframework.test.util.ReflectionTestUtils;
 
 import java.util.List;
+import java.util.Set;
 
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -106,6 +111,7 @@ class PosOrderShOprateControllerTest {
         ReflectionTestUtils.setField(controller, "posStoreService", stores);
         ReflectionTestUtils.setField(controller, "operatingHoursService", hours);
         ReflectionTestUtils.setField(controller, "orderParentService", parents);
+        wireMerchantSecurity(controller, 101L);
         when(users.getOne(any())).thenReturn(user(101L, "1", null));
         PosStore store = new PosStore();
         store.setId(9);
@@ -141,6 +147,7 @@ class PosOrderShOprateControllerTest {
         ReflectionTestUtils.setField(controller, "posOrderService", orders);
         ReflectionTestUtils.setField(controller, "linePayOrderGuard", guard);
         ReflectionTestUtils.setField(controller, "orderLogHelper", mock(OrderLogHelper.class));
+        wireMerchantSecurity(controller, 101L);
 
         PosOrder order = new PosOrder();
         order.setId(10L);
@@ -169,6 +176,7 @@ class PosOrderShOprateControllerTest {
         ReflectionTestUtils.setField(controller, "linePayOrderGuard", mock(LinePayOrderGuard.class));
         ReflectionTestUtils.setField(controller, "orderLogHelper", mock(OrderLogHelper.class));
         ReflectionTestUtils.setField(controller, "orderInvoiceService", mock(OrderInvoiceService.class));
+        wireMerchantSecurity(controller, 101L);
 
         PosOrder order = new PosOrder();
         order.setId(10L);
@@ -192,6 +200,86 @@ class PosOrderShOprateControllerTest {
         org.junit.jupiter.api.Assertions.assertNull(captor.getValue().getDeliveryStatus());
     }
 
+    @Test
+    void subaccountCannotOperateOrderFromUnauthorizedStoreEvenWhenOwnerMatches() {
+        PosOrderShOprateController controller = new PosOrderShOprateController();
+        IInfoUserService users = mock(IInfoUserService.class);
+        IPosOrderService orders = mock(IPosOrderService.class);
+        MerchantTokenSessionService sessions = mock(MerchantTokenSessionService.class);
+        MerchantStoreAccessService access = mock(MerchantStoreAccessService.class);
+        ReflectionTestUtils.setField(controller, "infoUserService", users);
+        ReflectionTestUtils.setField(controller, "posOrderService", orders);
+        ReflectionTestUtils.setField(controller, "merchantTokenSessionService", sessions);
+        ReflectionTestUtils.setField(controller, "merchantStoreAccessService", access);
+        ReflectionTestUtils.setField(controller, "linePayOrderGuard", mock(LinePayOrderGuard.class));
+
+        PosOrder order = new PosOrder();
+        order.setId(10L);
+        order.setMdId(99L);
+        order.setShId(101L);
+        order.setState(0L);
+        InfoUser subaccount = user(501L, "5", null);
+        subaccount.setMerchantOwnerId(101L);
+        when(sessions.requireUserId(anyString())).thenReturn(501L);
+        when(users.getOne(any())).thenReturn(subaccount);
+        when(orders.getOne(any())).thenReturn(order);
+        org.mockito.Mockito.doThrow(new ServiceException("merchant.store.access.denied"))
+                .when(access).requireOrderAccess(501L, 10L);
+
+        assertThrows(ServiceException.class,
+                () -> controller.acceptOrder(JwtUtil.token("501", "subaccount"), 10L));
+        verify(orders, never()).saveOrUpdate(any(PosOrder.class));
+    }
+
+    @Test
+    void unauthorizedSubaccountCannotReachCancellationRefundFlow() {
+        PosOrderShOprateController controller = new PosOrderShOprateController();
+        IInfoUserService users = mock(IInfoUserService.class);
+        IPosOrderService orders = mock(IPosOrderService.class);
+        MerchantTokenSessionService sessions = mock(MerchantTokenSessionService.class);
+        MerchantStoreAccessService access = mock(MerchantStoreAccessService.class);
+        LinePayRefundService refunds = mock(LinePayRefundService.class);
+        ReflectionTestUtils.setField(controller, "infoUserService", users);
+        ReflectionTestUtils.setField(controller, "posOrderService", orders);
+        ReflectionTestUtils.setField(controller, "merchantTokenSessionService", sessions);
+        ReflectionTestUtils.setField(controller, "merchantStoreAccessService", access);
+        ReflectionTestUtils.setField(controller, "linePayOrderGuard", mock(LinePayOrderGuard.class));
+        ReflectionTestUtils.setField(controller, "linePayRefundService", refunds);
+
+        PosOrder order = new PosOrder();
+        order.setId(10L);
+        order.setMdId(99L);
+        order.setShId(101L);
+        order.setState(0L);
+        InfoUser subaccount = user(501L, "5", null);
+        when(sessions.requireUserId(anyString())).thenReturn(501L);
+        when(users.getOne(any())).thenReturn(subaccount);
+        when(orders.getOne(any())).thenReturn(order);
+        org.mockito.Mockito.doThrow(new ServiceException("merchant.store.access.denied"))
+                .when(access).requireOrderAccess(501L, 10L);
+
+        assertThrows(ServiceException.class,
+                () -> controller.cancelOrder(JwtUtil.token("501", "subaccount"), 10L));
+        verify(orders, never()).update(any(), any());
+        verify(refunds, never()).createRefundIntent(any(), anyString());
+    }
+
+    @Test
+    void subaccountCreatesOrderAsOwnerForAuthorizedStore() {
+        PosOrderShOprateController controller = new PosOrderShOprateController();
+        MerchantStoreAccessService access = mock(MerchantStoreAccessService.class);
+        ReflectionTestUtils.setField(controller, "merchantStoreAccessService", access);
+        when(access.resolve(501L)).thenReturn(new MerchantAccessContext(501L, 101L, false, Set.of(9L)));
+        InfoUser subaccount = user(501L, "5", null);
+        subaccount.setMerchantOwnerId(101L);
+        OrderCreatItem item = item(999L, 9L);
+
+        controller.validateMerchantOrderInput(subaccount, input(item));
+
+        org.junit.jupiter.api.Assertions.assertEquals(101L, item.getShId());
+        verify(access).requireStoreAccess(501L, 9L);
+    }
+
     private static InfoUser user(Long id, String type, Long storeId) {
         InfoUser user = new InfoUser();
         user.setUserId(id);
@@ -226,4 +314,12 @@ class PosOrderShOprateControllerTest {
         ReflectionTestUtils.setField(controller, "posStoreService", stores);
         return controller;
     }
+
+    private static void wireMerchantSecurity(PosOrderShOprateController controller, Long userId) {
+        MerchantTokenSessionService sessions = mock(MerchantTokenSessionService.class);
+        MerchantStoreAccessService access = mock(MerchantStoreAccessService.class);
+        when(sessions.requireUserId(anyString())).thenReturn(userId);
+        ReflectionTestUtils.setField(controller, "merchantTokenSessionService", sessions);
+        ReflectionTestUtils.setField(controller, "merchantStoreAccessService", access);
+    }
 }