Browse Source

限制店铺商品操作到授权门店

qmj 1 day ago
parent
commit
dc595b9f84

+ 46 - 7
ruoyi-admin/src/main/java/com/ruoyi/app/mendian/FoodSpecController.java

@@ -4,12 +4,14 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.ruoyi.common.annotation.Anonymous;
+import com.ruoyi.app.user.MerchantTokenSessionService;
 import com.ruoyi.common.core.controller.BaseController;
 import com.ruoyi.common.core.domain.AjaxResult;
 import com.ruoyi.system.domain.FoodSpecs;
 import com.ruoyi.system.domain.FoodSpecsValue;
 import com.ruoyi.system.service.IFoodSpecsService;
 import com.ruoyi.system.service.IFoodSpecsValueService;
+import com.ruoyi.system.service.MerchantStoreAccessService;
 import com.ruoyi.system.utils.Auth;
 import com.ruoyi.system.utils.JwtUtil;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -37,6 +39,10 @@ public class FoodSpecController extends BaseController
 
     @Autowired
     private IFoodSpecsValueService foodSpecsValueService;
+    @Autowired
+    private MerchantStoreAccessService merchantStoreAccessService;
+    @Autowired
+    private MerchantTokenSessionService merchantTokenSessionService;
 
     /**
      * 规格组分页列表(每条带 foodSpecsItems)
@@ -44,10 +50,13 @@ public class FoodSpecController extends BaseController
     @Anonymous
     @Auth
     @GetMapping("/foodSpecPageList")
-    public AjaxResult foodSpecPageList(@RequestParam(defaultValue = "1") Integer pageNum,
+    public AjaxResult foodSpecPageList(@RequestHeader String token,
+                                       @RequestParam(defaultValue = "1") Integer pageNum,
                                        @RequestParam(defaultValue = "10") Integer pageSize,
                                        @RequestParam Long mdId,
                                        @RequestParam(required = false) String language) {
+        merchantStoreAccessService.requireStoreAccess(
+                merchantTokenSessionService.requireUserId(token), mdId);
         Page<FoodSpecs> page = new Page<>(pageNum, pageSize);
         LambdaQueryWrapper<FoodSpecs> qw = new LambdaQueryWrapper<>();
         qw.eq(FoodSpecs::getMdId, mdId);
@@ -70,11 +79,18 @@ public class FoodSpecController extends BaseController
     @Auth
     @PostMapping("/saveFoodSpec")
     @Transactional(rollbackFor = Exception.class)
-    public AjaxResult saveFoodSpec(@RequestBody List<FoodSpecs> list) {
+    public AjaxResult saveFoodSpec(@RequestHeader String token, @RequestBody List<FoodSpecs> list) {
         if (list == null || list.isEmpty()) {
             return error();
         }
+        Long userId = merchantTokenSessionService.requireUserId(token);
         for (FoodSpecs s : list) {
+            if (s.getId() == null || s.getId() <= 0) {
+                merchantStoreAccessService.requireStoreAccess(userId, s.getMdId());
+            } else {
+                FoodSpecs existing = requireSpecsAccess(userId, s.getId());
+                s.setMdId(existing.getMdId());
+            }
             if (s.getIsDelete() == null) {
                 s.setIsDelete(false);
             }
@@ -149,7 +165,8 @@ public class FoodSpecController extends BaseController
     @Anonymous
     @Auth
     @GetMapping("/getSpecs")
-    public AjaxResult getSpecs(@RequestParam Long id) {
+    public AjaxResult getSpecs(@RequestHeader String token, @RequestParam Long id) {
+        requireSpecsAccess(merchantTokenSessionService.requireUserId(token), id);
         FoodSpecs s = foodSpecsService.getById(id);
         if (s != null) {
             List<FoodSpecs> list = new ArrayList<>();
@@ -165,7 +182,8 @@ public class FoodSpecController extends BaseController
     @Anonymous
     @Auth
     @GetMapping("/deleteFoodSpec")
-    public AjaxResult deleteFoodSpec(@RequestParam Long id) {
+    public AjaxResult deleteFoodSpec(@RequestHeader String token, @RequestParam Long id) {
+        requireSpecsAccess(merchantTokenSessionService.requireUserId(token), id);
         FoodSpecs s = new FoodSpecs();
         s.setId(id);
         s.setIsDelete(true);
@@ -178,8 +196,10 @@ public class FoodSpecController extends BaseController
     @Anonymous
     @Auth
     @GetMapping("/getAvailableSpecsList")
-    public AjaxResult getAvailableSpecsList(@RequestParam Long mdId,
+    public AjaxResult getAvailableSpecsList(@RequestHeader String token, @RequestParam Long mdId,
                                             @RequestParam(required = false) String language) {
+        merchantStoreAccessService.requireStoreAccess(
+                merchantTokenSessionService.requireUserId(token), mdId);
         LambdaQueryWrapper<FoodSpecs> qw = new LambdaQueryWrapper<>();
         qw.eq(FoodSpecs::getMdId, mdId);
         qw.eq(FoodSpecs::getIsOpen, true);
@@ -199,7 +219,9 @@ public class FoodSpecController extends BaseController
     @Anonymous
     @Auth
     @GetMapping("/changeOpen")
-    public AjaxResult changeOpen(@RequestParam Long id, @RequestParam Boolean isOpen) {
+    public AjaxResult changeOpen(@RequestHeader String token,
+                                 @RequestParam Long id, @RequestParam Boolean isOpen) {
+        requireSpecsAccess(merchantTokenSessionService.requireUserId(token), id);
         FoodSpecs s = new FoodSpecs();
         s.setId(id);
         s.setIsOpen(isOpen);
@@ -212,13 +234,30 @@ public class FoodSpecController extends BaseController
     @Anonymous
     @Auth
     @GetMapping("/changeSpecValueOpen")
-    public AjaxResult changeSpecValueOpen(@RequestParam Long id, @RequestParam Boolean isOpen) {
+    public AjaxResult changeSpecValueOpen(@RequestHeader String token,
+                                          @RequestParam Long id, @RequestParam Boolean isOpen) {
+        Long userId = merchantTokenSessionService.requireUserId(token);
+        FoodSpecsValue existingValue = foodSpecsValueService.getById(id);
+        if (existingValue == null) {
+            return error();
+        }
+        requireSpecsAccess(userId, existingValue.getParentId());
         FoodSpecsValue v = new FoodSpecsValue();
         v.setId(id);
         v.setIsOpen(isOpen);
         return toAjax(foodSpecsValueService.updateById(v));
     }
 
+    private FoodSpecs requireSpecsAccess(Long userId, Long specsId) {
+        FoodSpecs specs = foodSpecsService.getById(specsId);
+        if (specs == null) {
+            throw new com.ruoyi.common.exception.ServiceException(
+                    com.ruoyi.common.utils.MessageUtils.message("merchant.store.access.denied"));
+        }
+        merchantStoreAccessService.requireStoreAccess(userId, specs.getMdId());
+        return specs;
+    }
+
     /**
      * 批量填充规格值到规格组(避免 N+1)
      *

+ 26 - 2
ruoyi-admin/src/main/java/com/ruoyi/app/mendian/PosFenleiController.java

@@ -7,9 +7,11 @@ import com.alibaba.fastjson.JSONObject;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.ruoyi.common.annotation.Anonymous;
 import com.ruoyi.common.utils.MessageUtils;
+import com.ruoyi.app.user.MerchantTokenSessionService;
 import com.ruoyi.system.domain.PosFood;
 import com.ruoyi.system.domain.PosStore;
 import com.ruoyi.system.service.IPosFoodService;
+import com.ruoyi.system.service.MerchantStoreAccessService;
 import com.ruoyi.system.utils.Auth;
 import com.ruoyi.system.utils.JwtUtil;
 import org.springframework.security.access.prepost.PreAuthorize;
@@ -38,6 +40,10 @@ public class PosFenleiController extends BaseController
     private IPosFenleiService posFenleiService;
     @Autowired  //商品
     private IPosFoodService posFoodService;
+    @Autowired
+    private MerchantStoreAccessService merchantStoreAccessService;
+    @Autowired
+    private MerchantTokenSessionService merchantTokenSessionService;
 
 
     /**
@@ -46,7 +52,13 @@ public class PosFenleiController extends BaseController
     @Anonymous
     @Auth
     @GetMapping("/delefenlei")
-    public AjaxResult delefenlei(@RequestParam String id){
+    public AjaxResult delefenlei(@RequestHeader String token, @RequestParam String id){
+        PosFenlei existing = posFenleiService.getById(Long.valueOf(id));
+        if (existing == null) {
+            return error(MessageUtils.message("no.data.not.exist"));
+        }
+        merchantStoreAccessService.requireStoreAccess(
+                merchantTokenSessionService.requireUserId(token), existing.getMendid());
         QueryWrapper<PosFood> query = new QueryWrapper<>();
         query.eq("fl_id",id);
         List<PosFood> fdlist = posFoodService.list(query);
@@ -60,8 +72,20 @@ public class PosFenleiController extends BaseController
      * 添加或修改商品分类
      */
     @Anonymous
+    @Auth
     @PostMapping("/addfenlei")
-    public AjaxResult addfenlei(@RequestBody PosFenlei posFenlei){
+    public AjaxResult addfenlei(@RequestHeader String token, @RequestBody PosFenlei posFenlei){
+        Long userId = merchantTokenSessionService.requireUserId(token);
+        if (posFenlei.getId() == null) {
+            merchantStoreAccessService.requireStoreAccess(userId, posFenlei.getMendid());
+        } else {
+            PosFenlei existing = posFenleiService.getById(posFenlei.getId());
+            if (existing == null) {
+                return error(MessageUtils.message("no.data.not.exist"));
+            }
+            merchantStoreAccessService.requireStoreAccess(userId, existing.getMendid());
+            posFenlei.setMendid(existing.getMendid());
+        }
         Boolean org = posFenleiService.saveOrUpdate(posFenlei);
         if(org){
             return success();

+ 41 - 18
ruoyi-admin/src/main/java/com/ruoyi/app/mendian/PosFoodController.java

@@ -8,7 +8,8 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.ruoyi.app.mendian.dto.FoodSearchOutput;
 import com.ruoyi.app.order.OrderInvoiceService;
-import com.ruoyi.app.utils.ImageCompressUtils;
+import com.ruoyi.app.utils.ImageCompressUtils;
+import com.ruoyi.app.user.MerchantTokenSessionService;
 import com.ruoyi.common.annotation.Anonymous;
 import com.ruoyi.common.annotation.Log;
 import com.ruoyi.common.core.controller.BaseController;
@@ -73,29 +74,47 @@ public class PosFoodController extends BaseController {
     private IFoodSpecRelationService foodSpecRelationService;
 
     @Autowired //订单电子发票(门店是否能开票)
-    private OrderInvoiceService orderInvoiceService;
+    private OrderInvoiceService orderInvoiceService;
+    @Autowired
+    private MerchantStoreAccessService merchantStoreAccessService;
+    @Autowired
+    private MerchantTokenSessionService merchantTokenSessionService;
 
     //删除商品
     @Anonymous
     @Auth
     @GetMapping("/delefood")
-    public AjaxResult delefood(@RequestParam String id) {
-        return toAjax(posFoodService.deletePosFoodById(Long.valueOf(id)));
+    public AjaxResult delefood(@RequestHeader String token, @RequestParam String id) {
+        merchantStoreAccessService.requireFoodAccess(
+                merchantTokenSessionService.requireUserId(token), Long.valueOf(id));
+        return toAjax(posFoodService.deletePosFoodById(Long.valueOf(id)));
     }
 
     //推荐商品
-    @Anonymous
-    @PutMapping("/changerecommend")
-    public AjaxResult changerecommend(@RequestBody PosFood posFood) {
-        return toAjax(posFoodService.saveOrUpdate(posFood));
+    @Anonymous
+    @Auth
+    @PutMapping("/changerecommend")
+    public AjaxResult changerecommend(@RequestHeader String token, @RequestBody PosFood posFood) {
+        PosFood existing = merchantStoreAccessService.requireFoodAccess(
+                merchantTokenSessionService.requireUserId(token), posFood.getId());
+        posFood.setMdid(existing.getMdid());
+        return toAjax(posFoodService.saveOrUpdate(posFood));
     }
 
     /**
      * 添加或修改商品
      */
-    @Anonymous
-    @PostMapping("/setposfood")
-    public AjaxResult setposfood(@RequestBody PosFood posFood) {
+    @Anonymous
+    @Auth
+    @PostMapping("/setposfood")
+    public AjaxResult setposfood(@RequestHeader String token, @RequestBody PosFood posFood) {
+        Long userId = merchantTokenSessionService.requireUserId(token);
+        if (posFood.getId() == null) {
+            merchantStoreAccessService.requireStoreAccess(userId, posFood.getMdid());
+        } else {
+            PosFood existing = merchantStoreAccessService.requireFoodAccess(userId, posFood.getId());
+            posFood.setMdid(existing.getMdid());
+        }
         Boolean org = posFoodService.saveOrUpdate(posFood);
         if (!org) {
             return error();
@@ -156,9 +175,12 @@ public class PosFoodController extends BaseController {
     @Anonymous
     @GetMapping("/getidlist")
     public AjaxResult getidlist(@RequestHeader String token, @RequestParam Integer id, @RequestParam(defaultValue = "") String language, @RequestParam(required = false) String stackingUp, @RequestParam(required = false) String name) {
-        JwtUtil jwtUtil = new JwtUtil();
-        String ids = jwtUtil.getusid(token);
-        InfoUser user = infoUserService.getById(ids);
+        Long userId = merchantTokenSessionService.requireUserId(token);
+        InfoUser user = infoUserService.getById(userId);
+        PosFenlei category = posFenleiService.getById(id);
+        if (category != null && ("1".equals(user.getUserType()) || "5".equals(user.getUserType()))) {
+            merchantStoreAccessService.requireStoreAccess(userId, category.getMendid());
+        }
         QueryWrapper<PosFood> queryWrapper = new QueryWrapper<>();
         queryWrapper.eq("fl_id", id);
         //普通用户显示审核通过的
@@ -422,8 +444,9 @@ public class PosFoodController extends BaseController {
 
 
 
-    @Anonymous
-    @GetMapping("/getFoodPageList")
+    @Anonymous
+    @Auth
+    @GetMapping("/getFoodPageList")
     public AjaxResult getFoodPageList(@RequestHeader String token,
                                       @RequestParam Integer page,
                                       @RequestParam Integer size,
@@ -431,8 +454,8 @@ public class PosFoodController extends BaseController {
                                       @RequestParam Long mdId,
                                       @RequestParam(defaultValue = "") String language,
                                       @RequestParam(defaultValue = "") String name) {
-        JwtUtil jwtUtil = new JwtUtil();
-        String id = jwtUtil.getusid(token);
+        Long id = merchantTokenSessionService.requireUserId(token);
+        merchantStoreAccessService.requireStoreAccess(id, mdId);
         IPage<PosFood> stlist = new Page<>(page, size);
         LambdaQueryWrapper<PosFood> queryWrapper = new LambdaQueryWrapper<>();
         queryWrapper.eq(PosFood::getFlId, flId);

+ 76 - 16
ruoyi-admin/src/main/java/com/ruoyi/app/mendian/PosStoreController.java

@@ -14,6 +14,7 @@ import com.ruoyi.app.service.UserService;
 import com.ruoyi.app.utils.DateUtil;
 import com.ruoyi.app.utils.ImageCompressUtils;
 import com.ruoyi.app.user.dto.StoreOutput;
+import com.ruoyi.app.user.MerchantTokenSessionService;
 import com.ruoyi.app.utils.PayPush;
 import com.ruoyi.common.annotation.Anonymous;
 import com.ruoyi.common.annotation.Log;
@@ -83,6 +84,12 @@ public class PosStoreController extends BaseController {
     private IPosStoreEzpayService posStoreEzpayService;
     @Autowired
     private UserService userService;
+    @Autowired
+    private MerchantStoreAccessService merchantStoreAccessService;
+    @Autowired
+    private MerchantTokenSessionService merchantTokenSessionService;
+    @Autowired
+    private IMerchantSubaccountStoreService merchantSubaccountStoreService;
 
 
 
@@ -119,15 +126,23 @@ public class PosStoreController extends BaseController {
     //删除我的门店
     @Anonymous
     @Auth
+    @Transactional(rollbackFor = Exception.class)
     @GetMapping("/delemendian")
-    public AjaxResult delemendian(@RequestParam String id) {
+    public AjaxResult delemendian(@RequestHeader String token, @RequestParam String id) {
+        Long userId = merchantTokenSessionService.requireUserId(token);
+        merchantStoreAccessService.requireOwner(userId);
+        merchantStoreAccessService.requireStoreAccess(userId, Long.valueOf(id));
         QueryWrapper<PosFenlei> queryWrapper = new QueryWrapper<>();
         queryWrapper.eq("mendid", id);
         List<PosFenlei> list = posFenleiService.list(queryWrapper);
         if (list.size() > 0) {
             return error(MessageUtils.message("no.mendian.exist.classify.not.del"));
         } else {
-            return toAjax(posStoreService.deletePosStoreById(Long.valueOf(id)));
+            int rows = posStoreService.deletePosStoreById(Long.valueOf(id));
+            if (rows > 0) {
+                merchantSubaccountStoreService.deleteByStoreId(Long.valueOf(id));
+            }
+            return toAjax(rows);
         }
     }
 
@@ -140,8 +155,7 @@ public class PosStoreController extends BaseController {
     public AjaxResult storelistlist(@RequestHeader String token,
                                     @RequestParam Integer page,
                                     @RequestParam Integer size) {
-        JwtUtil jwtUtil = new JwtUtil();
-        String id = jwtUtil.getusid(token);
+        Long id = merchantTokenSessionService.requireUserId(token);
         IPage<PosStore> stlist = new Page<>(page, size);
         QueryWrapper<PosStore> queryWrapper = new QueryWrapper<>();
 
@@ -154,7 +168,12 @@ public class PosStoreController extends BaseController {
                 queryWrapper.eq("id", -1); // 无关联摊位,返回空列表
             }
         } else {
-            queryWrapper.eq("user_id", id);
+            Set<Long> storeIds = merchantStoreAccessService.getAccessibleStoreIds(id);
+            if (storeIds.isEmpty()) {
+                queryWrapper.eq("id", -1);
+            } else {
+                queryWrapper.in("id", storeIds);
+            }
         }
 
         IPage<PosStore> list = posStoreService.page(stlist, queryWrapper);
@@ -177,10 +196,19 @@ public class PosStoreController extends BaseController {
     @Auth
     @GetMapping("/getmystorelist")
     public AjaxResult getmystorelist(@RequestHeader String token) {
-        JwtUtil jwtUtil = new JwtUtil();
-        String id = jwtUtil.getusid(token);
+        Long id = merchantTokenSessionService.requireUserId(token);
         QueryWrapper<PosStore> queryWrapper = new QueryWrapper<>();
-        queryWrapper.eq("user_id", id);
+        InfoUser loginUser = infoUserService.selectInfoUserByUserId(id);
+        if (loginUser != null && "4".equals(loginUser.getUserType())) {
+            queryWrapper.eq("id", loginUser.getStoreId() == null ? -1 : loginUser.getStoreId());
+        } else {
+            Set<Long> storeIds = merchantStoreAccessService.getAccessibleStoreIds(id);
+            if (storeIds.isEmpty()) {
+                queryWrapper.eq("id", -1);
+            } else {
+                queryWrapper.in("id", storeIds);
+            }
+        }
         List<PosStore> list = posStoreService.list(queryWrapper);
         return success(list);
     }
@@ -260,12 +288,23 @@ public class PosStoreController extends BaseController {
 
     //添加门店
     @Anonymous
+    @Auth
     @PostMapping("/addmendian")
     @Transactional
-    public AjaxResult addmendian(@RequestBody PosStore posStore) {
-        // 新增时校验商家审核状态(未审核不能新增门市);不强制 token,身份取请求体 userId
-        if (posStore.getId() == null && posStore.getUserId() != null) {
-            userService.checkUserStatus(posStore.getUserId());
+    public AjaxResult addmendian(@RequestHeader String token, @RequestBody PosStore posStore) {
+        Long loginUserId = merchantTokenSessionService.requireUserId(token);
+        InfoUser loginUser = infoUserService.selectInfoUserByUserId(loginUserId);
+        if (posStore.getId() == null) {
+            merchantStoreAccessService.requireOwner(loginUserId);
+            userService.checkUserStatus(loginUserId);
+            posStore.setUserId(loginUserId);
+        } else {
+            requireStoreAccess(loginUserId, loginUser, posStore.getId().longValue());
+            PosStore existing = posStoreService.getById(posStore.getId());
+            if (existing == null) {
+                throw new ServiceException(MessageUtils.message("merchant.store.access.denied"));
+            }
+            posStore.setUserId(existing.getUserId());
         }
         try {
             // 1. 先保存门店信息
@@ -296,8 +335,11 @@ public class PosStoreController extends BaseController {
                 posStoreEzpayService.uploadUbn(store.getId().longValue(), posStore.getUbn());
             }
             return success(MessageUtils.message("no.success"), store);
-        } catch (Exception e) {
-            return error("添加门店失败: " + e.getMessage());
+        } catch (ServiceException exception) {
+            throw exception;
+        } catch (Exception exception) {
+            logger.error("保存商家门店失败,userId:{},storeId:{}", loginUserId, posStore.getId(), exception);
+            return error(MessageUtils.message("no.system.error"));
         }
     }
 
@@ -342,7 +384,9 @@ public class PosStoreController extends BaseController {
     @Anonymous
     @Auth
     @GetMapping("/getMdBusinessHours")
-    public AjaxResult getMdBusinessHours(@RequestParam Long mdId){
+    public AjaxResult getMdBusinessHours(@RequestHeader String token, @RequestParam Long mdId){
+        Long userId = merchantTokenSessionService.requireUserId(token);
+        requireStoreAccess(userId, infoUserService.selectInfoUserByUserId(userId), mdId);
         LambdaQueryWrapper<OperatingHours> query=new LambdaQueryWrapper<>();
         query.eq(OperatingHours::getMdId,mdId);
         List<OperatingHours> list = operatingHoursService.list(query);
@@ -361,11 +405,17 @@ public class PosStoreController extends BaseController {
     @Anonymous
     @Auth
     @PostMapping("/saveMdBusinessHours")
-    public AjaxResult saveMdBusinessHours(@RequestBody List<StoreOperatOutput> businessHours) {
+    public AjaxResult saveMdBusinessHours(@RequestHeader String token,
+                                          @RequestBody List<StoreOperatOutput> businessHours) {
         if(businessHours == null || businessHours.isEmpty()) {
             throw new ServiceException(MessageUtils.message("no.mendian.not.set.business.hours"));
         }
         Long mdId = businessHours.get(0).getMdId();
+        Long userId = merchantTokenSessionService.requireUserId(token);
+        requireStoreAccess(userId, infoUserService.selectInfoUserByUserId(userId), mdId);
+        if (businessHours.stream().anyMatch(item -> !Objects.equals(mdId, item.getMdId()))) {
+            throw new ServiceException(MessageUtils.message("merchant.store.access.denied"));
+        }
         PosStore store = posStoreService.getById(businessHours.get(0).getMdId());
         // 先删除旧的营业时间记录
         LambdaQueryWrapper<OperatingHours> query = new LambdaQueryWrapper<>();
@@ -392,6 +442,16 @@ public class PosStoreController extends BaseController {
         return success();
     }
 
+    private void requireStoreAccess(Long loginUserId, InfoUser loginUser, Long storeId) {
+        if (loginUser != null && "4".equals(loginUser.getUserType())) {
+            if (!Objects.equals(loginUser.getStoreId(), storeId)) {
+                throw new ServiceException(MessageUtils.message("merchant.store.access.denied"));
+            }
+            return;
+        }
+        merchantStoreAccessService.requireStoreAccess(loginUserId, storeId);
+    }
+
     //返回营业时间(按星期分组)
     private List<StoreOperatOutput> getStoreOperatOutput(List<OperatingHours> list, Long mdId){
         List<StoreOperatOutput> data= new ArrayList<>();

+ 85 - 0
ruoyi-admin/src/test/java/com/ruoyi/app/mendian/PosFoodControllerAccessTest.java

@@ -0,0 +1,85 @@
+package com.ruoyi.app.mendian;
+
+import com.ruoyi.app.user.MerchantTokenSessionService;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.MessageUtils;
+import com.ruoyi.system.domain.PosFood;
+import com.ruoyi.system.service.IFoodSpecRelationService;
+import com.ruoyi.system.service.IFoodSpecsService;
+import com.ruoyi.system.service.IFoodSpecsValueService;
+import com.ruoyi.system.service.IPosFoodService;
+import com.ruoyi.system.service.MerchantStoreAccessService;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.MockedStatic;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+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 PosFoodControllerAccessTest {
+
+    private PosFoodController controller;
+    private IPosFoodService posFoodService;
+    private MerchantStoreAccessService accessService;
+    private MerchantTokenSessionService tokenSessionService;
+    private MockedStatic<MessageUtils> messages;
+
+    @BeforeEach
+    void setUp() {
+        messages = mockStatic(MessageUtils.class);
+        messages.when(() -> MessageUtils.message(org.mockito.ArgumentMatchers.anyString()))
+                .thenAnswer(invocation -> invocation.getArgument(0));
+        controller = new PosFoodController();
+        posFoodService = mock(IPosFoodService.class);
+        accessService = mock(MerchantStoreAccessService.class);
+        tokenSessionService = mock(MerchantTokenSessionService.class);
+        ReflectionTestUtils.setField(controller, "posFoodService", posFoodService);
+        ReflectionTestUtils.setField(controller, "merchantStoreAccessService", accessService);
+        ReflectionTestUtils.setField(controller, "merchantTokenSessionService", tokenSessionService);
+        ReflectionTestUtils.setField(controller, "foodSpecsService", mock(IFoodSpecsService.class));
+        ReflectionTestUtils.setField(controller, "foodSpecsValueService", mock(IFoodSpecsValueService.class));
+        ReflectionTestUtils.setField(controller, "foodSpecRelationService", mock(IFoodSpecRelationService.class));
+        when(tokenSessionService.requireUserId("token")).thenReturn(5L);
+        when(posFoodService.saveOrUpdate(any(PosFood.class))).thenReturn(true);
+    }
+
+    @AfterEach
+    void tearDown() {
+        messages.close();
+    }
+
+    @Test
+    void updateUsesPersistedFoodStoreInsteadOfSpoofedStore() {
+        PosFood persisted = new PosFood();
+        persisted.setId(20L);
+        persisted.setMdid(10L);
+        when(accessService.requireFoodAccess(5L, 20L)).thenReturn(persisted);
+        PosFood request = new PosFood();
+        request.setId(20L);
+        request.setMdid(999L);
+
+        controller.setposfood("token", request);
+
+        ArgumentCaptor<PosFood> captor = ArgumentCaptor.forClass(PosFood.class);
+        verify(posFoodService).saveOrUpdate(captor.capture());
+        assertEquals(10L, captor.getValue().getMdid());
+    }
+
+    @Test
+    void newFoodCannotUseUnauthorizedStore() {
+        PosFood request = new PosFood();
+        request.setMdid(999L);
+        org.mockito.Mockito.doThrow(new ServiceException("denied"))
+                .when(accessService).requireStoreAccess(5L, 999L);
+
+        assertThrows(ServiceException.class, () -> controller.setposfood("token", request));
+    }
+}

+ 18 - 2
ruoyi-admin/src/test/java/com/ruoyi/app/mendian/PosStoreControllerTest.java

@@ -2,12 +2,15 @@ package com.ruoyi.app.mendian;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.ruoyi.app.service.UserService;
+import com.ruoyi.app.user.MerchantTokenSessionService;
 import com.ruoyi.common.exception.ServiceException;
 import com.ruoyi.common.utils.MessageUtils;
 import com.ruoyi.framework.web.exception.GlobalExceptionHandler;
 import com.ruoyi.system.domain.InfoUser;
 import com.ruoyi.system.domain.PosStore;
 import com.ruoyi.system.service.IPosStoreService;
+import com.ruoyi.system.service.IInfoUserService;
+import com.ruoyi.system.service.MerchantStoreAccessService;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -32,9 +35,18 @@ class PosStoreControllerTest {
     @Mock
     private IPosStoreService posStoreService;
 
+    @Mock
+    private IInfoUserService infoUserService;
+
     @Mock
     private UserService userService;
 
+    @Mock
+    private MerchantTokenSessionService merchantTokenSessionService;
+
+    @Mock
+    private MerchantStoreAccessService merchantStoreAccessService;
+
     @InjectMocks
     private PosStoreController controller;
 
@@ -54,6 +66,7 @@ class PosStoreControllerTest {
                 .build();
         objectMapper = new ObjectMapper();
         when(posStoreService.saveOrUpdate(any(PosStore.class))).thenReturn(true);
+        when(merchantTokenSessionService.requireUserId("token")).thenReturn(101L);
     }
 
     @AfterEach
@@ -68,6 +81,7 @@ class PosStoreControllerTest {
                 .thenThrow(new ServiceException("账号状态未审核"));
 
         mockMvc.perform(post("/chanting/store/addmendian")
+                        .header("token", "token")
                         .contentType(APPLICATION_JSON)
                         .content(objectMapper.writeValueAsString(newStore(101L))))
                 .andExpect(jsonPath("$.code").value(500))
@@ -75,15 +89,16 @@ class PosStoreControllerTest {
     }
 
     @Test
-    void auditedMerchantCreatesStoreOwnedByRequestBodyUser() throws Exception {
+    void auditedMerchantCreatesStoreOwnedByAuthenticatedOwner() throws Exception {
         when(userService.checkUserStatus(101L)).thenReturn(auditedMerchant(101L));
         PosStore saved = newStore(101L);
         saved.setId(9);
         when(posStoreService.getOne(any())).thenReturn(saved);
 
         mockMvc.perform(post("/chanting/store/addmendian")
+                        .header("token", "token")
                         .contentType(APPLICATION_JSON)
-                        .content(objectMapper.writeValueAsString(newStore(101L))))
+                        .content(objectMapper.writeValueAsString(newStore(999L))))
                 .andExpect(jsonPath("$.code").value(200));
 
         ArgumentCaptor<PosStore> captor = ArgumentCaptor.forClass(PosStore.class);
@@ -100,6 +115,7 @@ class PosStoreControllerTest {
         when(posStoreService.getById(9)).thenReturn(existing);
 
         mockMvc.perform(post("/chanting/store/addmendian")
+                        .header("token", "token")
                         .contentType(APPLICATION_JSON)
                         .content(objectMapper.writeValueAsString(existing)))
                 .andExpect(jsonPath("$.code").value(200));