Browse Source

集中校验商家门店数据权限

qmj 1 day ago
parent
commit
e63cb3af90

+ 4 - 0
ruoyi-system/src/main/java/com/ruoyi/system/mapper/PosStoreMapper.java

@@ -44,6 +44,10 @@ public interface PosStoreMapper extends BaseMapper<PosStore>
     @Select("SELECT IFNULL(COUNT(*),0) FROM pos_store where del_flag='0' and user_id = #{id}")  //查询语句
     int getshstore( @Param("id") Long id);  //反回结果
 
+    /** 查询普通商家当前未删除门店ID */
+    @Select("SELECT id FROM pos_store WHERE user_id = #{userId} AND del_flag = '0' ORDER BY id")
+    List<Long> selectOwnedStoreIdsByUserId(@Param("userId") Long userId);
+
     //按照评分查询门店
     @Select("SELECT *,(st_distance(point(longitude,latitude),point(#{longitude},#{latitude}))*111195/1000 ) as juli,(SELECT del_flag FROM info_user WHERE info_user.user_id = pos_store.user_id) as xxzt,(SELECT IFNULL(AVG(score),0) FROM pos_review WHERE pos_review.md_id = pos_store.id) as pingf,(SELECT IFNULL(COUNT(id),0) FROM pos_order WHERE pos_order.md_id = pos_store.id) as ddsl  FROM pos_store WHERE del_flag='0' and off_shelf='0' HAVING xxzt=0 ORDER BY pingf DESC limit #{page},10")  //查询语句
     List<PosStore> getPingfStore(@Param("longitude")BigDecimal longitude, @Param("latitude")BigDecimal latitude,@Param("page")Integer page,@Param("juli")Integer juli);  //反回结果

+ 40 - 0
ruoyi-system/src/main/java/com/ruoyi/system/service/MerchantAccessContext.java

@@ -0,0 +1,40 @@
+package com.ruoyi.system.service;
+
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+/**
+ * 一次请求内解析出的商家门店访问上下文。
+ */
+public final class MerchantAccessContext {
+
+    private final Long loginUserId;
+    private final Long ownerUserId;
+    private final boolean owner;
+    private final Set<Long> storeIds;
+
+    public MerchantAccessContext(Long loginUserId, Long ownerUserId,
+                                 boolean owner, Set<Long> storeIds) {
+        this.loginUserId = loginUserId;
+        this.ownerUserId = ownerUserId;
+        this.owner = owner;
+        this.storeIds = Collections.unmodifiableSet(new LinkedHashSet<>(storeIds));
+    }
+
+    public Long loginUserId() {
+        return loginUserId;
+    }
+
+    public Long ownerUserId() {
+        return ownerUserId;
+    }
+
+    public boolean owner() {
+        return owner;
+    }
+
+    public Set<Long> storeIds() {
+        return storeIds;
+    }
+}

+ 133 - 0
ruoyi-system/src/main/java/com/ruoyi/system/service/MerchantStoreAccessService.java

@@ -0,0 +1,133 @@
+package com.ruoyi.system.service;
+
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.MessageUtils;
+import com.ruoyi.system.domain.InfoUser;
+import com.ruoyi.system.domain.PosFood;
+import com.ruoyi.system.domain.PosOrder;
+import com.ruoyi.system.domain.PosStore;
+import com.ruoyi.system.domain.constants.MerchantAccountConstants;
+import com.ruoyi.system.mapper.InfoUserMapper;
+import com.ruoyi.system.mapper.MerchantSubaccountStoreMapper;
+import com.ruoyi.system.mapper.PosFoodMapper;
+import com.ruoyi.system.mapper.PosOrderMapper;
+import com.ruoyi.system.mapper.PosStoreMapper;
+import org.springframework.stereotype.Service;
+
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * 商家主账号和分管账号共用的门店级访问控制。
+ */
+@Service
+public class MerchantStoreAccessService {
+
+    private final InfoUserMapper infoUserMapper;
+    private final MerchantSubaccountStoreMapper relationMapper;
+    private final PosStoreMapper posStoreMapper;
+    private final PosOrderMapper posOrderMapper;
+    private final PosFoodMapper posFoodMapper;
+
+    public MerchantStoreAccessService(InfoUserMapper infoUserMapper,
+                                      MerchantSubaccountStoreMapper relationMapper,
+                                      PosStoreMapper posStoreMapper,
+                                      PosOrderMapper posOrderMapper,
+                                      PosFoodMapper posFoodMapper) {
+        this.infoUserMapper = infoUserMapper;
+        this.relationMapper = relationMapper;
+        this.posStoreMapper = posStoreMapper;
+        this.posOrderMapper = posOrderMapper;
+        this.posFoodMapper = posFoodMapper;
+    }
+
+    public MerchantAccessContext resolve(Long loginUserId) {
+        InfoUser loginUser = requireActiveUser(loginUserId);
+        if (MerchantAccountConstants.OWNER_USER_TYPE.equals(loginUser.getUserType())) {
+            List<Long> owned = posStoreMapper.selectOwnedStoreIdsByUserId(loginUserId);
+            return new MerchantAccessContext(loginUserId, loginUserId, true,
+                    owned == null ? Set.of() : new LinkedHashSet<>(owned));
+        }
+        if (!MerchantAccountConstants.SUBACCOUNT_USER_TYPE.equals(loginUser.getUserType())
+                || !MerchantAccountConstants.STATUS_ENABLED.equals(loginUser.getSubaccountStatus())
+                || loginUser.getMerchantOwnerId() == null) {
+            throw denied("merchant.account.unavailable");
+        }
+
+        InfoUser owner = infoUserMapper.selectInfoUserByUserId(loginUser.getMerchantOwnerId());
+        if (!isActiveOwner(owner)) {
+            throw denied("merchant.owner.unavailable");
+        }
+
+        Set<Long> accessible = new LinkedHashSet<>();
+        List<Long> authorized = relationMapper.selectStoreIdsBySubaccountUserId(loginUserId);
+        if (authorized != null) {
+            for (Long storeId : authorized) {
+                PosStore store = storeId == null ? null : posStoreMapper.selectPosStoreById(storeId);
+                if (store != null
+                        && MerchantAccountConstants.NOT_DELETED.equals(store.getDelFlag())
+                        && Objects.equals(owner.getUserId(), store.getUserId())) {
+                    accessible.add(storeId);
+                }
+            }
+        }
+        return new MerchantAccessContext(loginUserId, owner.getUserId(), false, accessible);
+    }
+
+    public void requireOwner(Long loginUserId) {
+        if (!resolve(loginUserId).owner()) {
+            throw denied("merchant.owner.required");
+        }
+    }
+
+    public Set<Long> getAccessibleStoreIds(Long loginUserId) {
+        return resolve(loginUserId).storeIds();
+    }
+
+    public void requireStoreAccess(Long loginUserId, Long storeId) {
+        if (storeId == null || !resolve(loginUserId).storeIds().contains(storeId)) {
+            throw denied("merchant.store.access.denied");
+        }
+    }
+
+    public PosOrder requireOrderAccess(Long loginUserId, Long orderId) {
+        PosOrder order = orderId == null ? null : posOrderMapper.selectPosOrderById(orderId);
+        if (order == null) {
+            throw denied("merchant.order.not.found");
+        }
+        requireStoreAccess(loginUserId, order.getMdId());
+        return order;
+    }
+
+    public PosFood requireFoodAccess(Long loginUserId, Long foodId) {
+        PosFood food = foodId == null ? null : posFoodMapper.selectPosFoodById(foodId);
+        if (food == null) {
+            throw denied("merchant.food.not.found");
+        }
+        requireStoreAccess(loginUserId, food.getMdid());
+        return food;
+    }
+
+    private InfoUser requireActiveUser(Long loginUserId) {
+        InfoUser user = loginUserId == null ? null : infoUserMapper.selectInfoUserByUserId(loginUserId);
+        if (user == null
+                || !MerchantAccountConstants.NOT_DELETED.equals(user.getDelFlag())
+                || !MerchantAccountConstants.STATUS_ENABLED.equals(user.getStatus())) {
+            throw denied("merchant.account.unavailable");
+        }
+        return user;
+    }
+
+    private boolean isActiveOwner(InfoUser owner) {
+        return owner != null
+                && MerchantAccountConstants.OWNER_USER_TYPE.equals(owner.getUserType())
+                && MerchantAccountConstants.STATUS_ENABLED.equals(owner.getStatus())
+                && MerchantAccountConstants.NOT_DELETED.equals(owner.getDelFlag());
+    }
+
+    private ServiceException denied(String messageKey) {
+        return new ServiceException(MessageUtils.message(messageKey));
+    }
+}

+ 144 - 0
ruoyi-system/src/test/java/com/ruoyi/system/service/MerchantStoreAccessServiceTest.java

@@ -0,0 +1,144 @@
+package com.ruoyi.system.service;
+
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.MessageUtils;
+import com.ruoyi.system.domain.InfoUser;
+import com.ruoyi.system.domain.PosFood;
+import com.ruoyi.system.domain.PosOrder;
+import com.ruoyi.system.domain.PosStore;
+import com.ruoyi.system.mapper.InfoUserMapper;
+import com.ruoyi.system.mapper.MerchantSubaccountStoreMapper;
+import com.ruoyi.system.mapper.PosFoodMapper;
+import com.ruoyi.system.mapper.PosOrderMapper;
+import com.ruoyi.system.mapper.PosStoreMapper;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.List;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class MerchantStoreAccessServiceTest {
+
+    @Mock private InfoUserMapper infoUserMapper;
+    @Mock private MerchantSubaccountStoreMapper relationMapper;
+    @Mock private PosStoreMapper posStoreMapper;
+    @Mock private PosOrderMapper posOrderMapper;
+    @Mock private PosFoodMapper posFoodMapper;
+
+    private MockedStatic<MessageUtils> messages;
+    private MerchantStoreAccessService service;
+
+    @BeforeEach
+    void setUp() {
+        messages = mockStatic(MessageUtils.class);
+        messages.when(() -> MessageUtils.message(org.mockito.ArgumentMatchers.anyString()))
+                .thenAnswer(invocation -> invocation.getArgument(0));
+        service = new MerchantStoreAccessService(infoUserMapper, relationMapper,
+                posStoreMapper, posOrderMapper, posFoodMapper);
+    }
+
+    @AfterEach
+    void tearDown() {
+        messages.close();
+    }
+
+    @Test
+    void ownerReceivesAllCurrentlyOwnedStores() {
+        when(infoUserMapper.selectInfoUserByUserId(1L)).thenReturn(user(1L, "1", "0", null, null));
+        when(posStoreMapper.selectOwnedStoreIdsByUserId(1L)).thenReturn(List.of(10L, 11L));
+
+        MerchantAccessContext context = service.resolve(1L);
+
+        assertEquals(1L, context.ownerUserId());
+        assertEquals(Set.of(10L, 11L), context.storeIds());
+        assertEquals(true, context.owner());
+    }
+
+    @Test
+    void subaccountOnlyReceivesAuthorizedStoresStillOwnedByItsOwner() {
+        when(infoUserMapper.selectInfoUserByUserId(5L)).thenReturn(user(5L, "5", "0", "0", 1L));
+        when(infoUserMapper.selectInfoUserByUserId(1L)).thenReturn(user(1L, "1", "0", null, null));
+        when(relationMapper.selectStoreIdsBySubaccountUserId(5L)).thenReturn(List.of(10L, 11L));
+        when(posStoreMapper.selectPosStoreById(10L)).thenReturn(store(10, 1L, "0"));
+        when(posStoreMapper.selectPosStoreById(11L)).thenReturn(store(11, 2L, "0"));
+
+        assertEquals(Set.of(10L), service.getAccessibleStoreIds(5L));
+    }
+
+    @Test
+    void disabledSubaccountOrOwnerIsRejected() {
+        when(infoUserMapper.selectInfoUserByUserId(5L)).thenReturn(user(5L, "5", "0", "1", 1L));
+        assertThrows(ServiceException.class, () -> service.resolve(5L));
+
+        when(infoUserMapper.selectInfoUserByUserId(6L)).thenReturn(user(6L, "5", "0", "0", 2L));
+        when(infoUserMapper.selectInfoUserByUserId(2L)).thenReturn(user(2L, "1", "1", null, null));
+        assertThrows(ServiceException.class, () -> service.resolve(6L));
+    }
+
+    @Test
+    void requireOwnerRejectsSubaccount() {
+        when(infoUserMapper.selectInfoUserByUserId(5L)).thenReturn(user(5L, "5", "0", "0", 1L));
+        when(infoUserMapper.selectInfoUserByUserId(1L)).thenReturn(user(1L, "1", "0", null, null));
+        when(relationMapper.selectStoreIdsBySubaccountUserId(5L)).thenReturn(List.of());
+
+        assertThrows(ServiceException.class, () -> service.requireOwner(5L));
+    }
+
+    @Test
+    void orderAccessUsesPersistedOrderStoreInsteadOfRequestStore() {
+        when(infoUserMapper.selectInfoUserByUserId(5L)).thenReturn(user(5L, "5", "0", "0", 1L));
+        when(infoUserMapper.selectInfoUserByUserId(1L)).thenReturn(user(1L, "1", "0", null, null));
+        when(relationMapper.selectStoreIdsBySubaccountUserId(5L)).thenReturn(List.of(10L));
+        when(posStoreMapper.selectPosStoreById(10L)).thenReturn(store(10, 1L, "0"));
+        PosOrder order = new PosOrder();
+        order.setId(100L);
+        order.setMdId(11L);
+        when(posOrderMapper.selectPosOrderById(100L)).thenReturn(order);
+
+        assertThrows(ServiceException.class, () -> service.requireOrderAccess(5L, 100L));
+    }
+
+    @Test
+    void foodAccessUsesPersistedFoodStore() {
+        when(infoUserMapper.selectInfoUserByUserId(5L)).thenReturn(user(5L, "5", "0", "0", 1L));
+        when(infoUserMapper.selectInfoUserByUserId(1L)).thenReturn(user(1L, "1", "0", null, null));
+        when(relationMapper.selectStoreIdsBySubaccountUserId(5L)).thenReturn(List.of(10L));
+        when(posStoreMapper.selectPosStoreById(10L)).thenReturn(store(10, 1L, "0"));
+        PosFood food = new PosFood();
+        food.setId(200L);
+        food.setMdid(10L);
+        when(posFoodMapper.selectPosFoodById(200L)).thenReturn(food);
+
+        assertEquals(food, service.requireFoodAccess(5L, 200L));
+    }
+
+    private InfoUser user(Long id, String type, String status, String subaccountStatus, Long ownerId) {
+        InfoUser user = new InfoUser();
+        user.setUserId(id);
+        user.setUserType(type);
+        user.setStatus(status);
+        user.setSubaccountStatus(subaccountStatus);
+        user.setMerchantOwnerId(ownerId);
+        user.setDelFlag("0");
+        return user;
+    }
+
+    private PosStore store(Integer id, Long userId, String delFlag) {
+        PosStore store = new PosStore();
+        store.setId(id);
+        store.setUserId(userId);
+        store.setDelFlag(delFlag);
+        return store;
+    }
+}