fix(权限): 店铺数据重复检查仅勾菜单无「店铺数据记录」仍403——校验口径与自动补权对齐(任务/重复检查任一菜单+数据权限即可放行,镜像魏振峰反馈: 只勾103未勾94且数据权限已补仍被拒); 顺带修复 controller 直连 mapper 架构违规 resolveVisibleShopNameKeys 下沉 ShopDataCrawlAdminTasksService
This commit is contained in:
+14
-3
@@ -377,7 +377,15 @@ public class PermissionMenuService {
|
||||
return requestedIds.size();
|
||||
}
|
||||
|
||||
/** Verifies both the visible admin menu and its separately managed data grant. */
|
||||
/**
|
||||
* Verifies a visible shop-data admin menu and its separately managed data grant.
|
||||
*
|
||||
* <p>校验口径与 {@link #autoGrantShopDataDataPermission} 保持一致:「店铺数据记录」
|
||||
* {@code admin_shop_data_crawl_tasks} 或「店铺数据重复检查」{@code admin_shop_data_duplicate_check}
|
||||
* 任一可见菜单,并同时持有内部数据权限 {@code admin_shop_data_crawl_task_data}。
|
||||
* 只勾了「店铺数据重复检查」而未勾「店铺数据记录」的管理员(数据权限已被自动补上)
|
||||
* 不应再被 403 拒绝。</p>
|
||||
*/
|
||||
public void requireShopDataCrawlTaskAccess(AdminUserEntity operator) {
|
||||
ensureAdminOperatorIfPresent(operator);
|
||||
if (operator == null || operator.getId() == null || operator.getId() <= 0) {
|
||||
@@ -388,10 +396,13 @@ public class PermissionMenuService {
|
||||
}
|
||||
PermissionMenuEntity taskMenu = requireDataPermission(
|
||||
"admin_shop_data_crawl_tasks", "店铺数据任务管理菜单");
|
||||
PermissionMenuEntity duplicateCheckMenu = requireDataPermission(
|
||||
"admin_shop_data_duplicate_check", "店铺数据重复检查菜单");
|
||||
PermissionMenuEntity dataPermission = requireDataPermission(
|
||||
SHOP_DATA_CRAWL_DATA_PERMISSION_KEY, "店铺数据任务数据");
|
||||
if (!hasEffectiveColumnPermission(operator.getId(), taskMenu.getId())
|
||||
|| !hasEffectiveColumnPermission(operator.getId(), dataPermission.getId())) {
|
||||
boolean hasShopDataMenu = hasEffectiveColumnPermission(operator.getId(), taskMenu.getId())
|
||||
|| hasEffectiveColumnPermission(operator.getId(), duplicateCheckMenu.getId());
|
||||
if (!hasShopDataMenu || !hasEffectiveColumnPermission(operator.getId(), dataPermission.getId())) {
|
||||
throw new BusinessException(403, "无权查看店铺数据任务");
|
||||
}
|
||||
}
|
||||
|
||||
+1
-12
@@ -7,7 +7,6 @@ import com.nanri.aiimage.modules.admin.support.AdminAuthSupport;
|
||||
import com.nanri.aiimage.modules.permission.model.entity.AdminUserEntity;
|
||||
import com.nanri.aiimage.modules.permission.service.PermissionMenuService;
|
||||
import com.nanri.aiimage.modules.shopdatacrawl.model.dto.ShopDataCrawlDownloadRowDto;
|
||||
import com.nanri.aiimage.modules.shopduplicatecheck.mapper.ShopDuplicateCheckSourceMapper;
|
||||
import com.nanri.aiimage.modules.shopdatacrawl.service.ShopDataCrawlAdminTasksService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@@ -39,7 +38,6 @@ import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
@@ -77,7 +75,6 @@ public class AdminShopDataCrawlTasksController {
|
||||
private final AdminAuthSupport adminAuthSupport;
|
||||
private final PermissionMenuService permissionMenuService;
|
||||
private final ShopDataCrawlAdminTasksService adminTasksService;
|
||||
private final ShopDuplicateCheckSourceMapper duplicateCheckSourceMapper;
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "店铺数据抓取记录:按店铺分组分页列表(每店最新结果文件)")
|
||||
@@ -256,15 +253,7 @@ public class AdminShopDataCrawlTasksController {
|
||||
|
||||
/** 非超管返回其可管店铺名(trim+小写去重,与撞款可见口径一致);超管调用方传 null 表示全量。 */
|
||||
private List<String> resolveVisibleShopNameKeys(Long operatorId) {
|
||||
List<String> names = duplicateCheckSourceMapper.selectManagedShopNames(operatorId);
|
||||
if (names == null || names.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return names.stream()
|
||||
.filter(name -> name != null && !name.isBlank())
|
||||
.map(name -> name.trim().toLowerCase(Locale.ROOT))
|
||||
.distinct()
|
||||
.toList();
|
||||
return adminTasksService.resolveVisibleShopNameKeys(operatorId);
|
||||
}
|
||||
|
||||
private AdminUserEntity resolveInternalOperator(HttpServletRequest request) {
|
||||
|
||||
+15
@@ -9,6 +9,7 @@ import com.nanri.aiimage.modules.shopdatacrawl.model.dto.ShopDataCrawlAdminGroup
|
||||
import com.nanri.aiimage.modules.shopdatacrawl.model.dto.ShopDataCrawlAdminGroupRow;
|
||||
import com.nanri.aiimage.modules.shopdatacrawl.model.dto.ShopDataCrawlAdminRow;
|
||||
import com.nanri.aiimage.modules.shopdatacrawl.model.dto.ShopDataCrawlDownloadRowDto;
|
||||
import com.nanri.aiimage.modules.shopduplicatecheck.mapper.ShopDuplicateCheckSourceMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -38,6 +39,7 @@ public class ShopDataCrawlAdminTasksService {
|
||||
private static final Set<String> SUPPORTED_COUNTRIES = Set.of("DE", "UK", "FR", "IT", "ES");
|
||||
|
||||
private final ShopDataCrawlAdminTasksMapper adminTasksMapper;
|
||||
private final ShopDuplicateCheckSourceMapper duplicateCheckSourceMapper;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
@@ -94,6 +96,19 @@ public class ShopDataCrawlAdminTasksService {
|
||||
return adminTasksMapper.selectDownloadRows(p);
|
||||
}
|
||||
|
||||
/** 非超管可管店铺名(trim+小写去重,与撞款可见口径一致);超管调用方传 null 表示全量。 */
|
||||
public List<String> resolveVisibleShopNameKeys(Long operatorId) {
|
||||
List<String> names = duplicateCheckSourceMapper.selectManagedShopNames(operatorId);
|
||||
if (names == null || names.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return names.stream()
|
||||
.filter(name -> name != null && !name.isBlank())
|
||||
.map(name -> name.trim().toLowerCase(Locale.ROOT))
|
||||
.distinct()
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> assembleGroupItems(Map<String, Object> p, List<ShopDataCrawlAdminGroupRow> groups) {
|
||||
List<String> shopNames = new ArrayList<>();
|
||||
for (ShopDataCrawlAdminGroupRow group : groups) {
|
||||
|
||||
+126
@@ -739,6 +739,132 @@ class PermissionMenuServiceTest {
|
||||
assertThat(captor.getValue().getColumnId()).isEqualTo(10L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shopDataTaskAccessAllowsDuplicateCheckMenuOnlyWithDataPermission() {
|
||||
// 反馈场景(魏振峰):只勾「店铺数据重复检查」菜单、数据权限已自动补上,
|
||||
// 虽未勾「店铺数据记录」菜单,仍应放行(校验口径与 autoGrant 保持一致)。
|
||||
PermissionMenuMapper menuMapper = mock(PermissionMenuMapper.class);
|
||||
UserColumnPermissionMapper permissionMapper = mock(UserColumnPermissionMapper.class);
|
||||
AdminUserMapper userMapper = mock(AdminUserMapper.class);
|
||||
PermissionMenuService service = new PermissionMenuService(menuMapper, permissionMapper, userMapper);
|
||||
|
||||
PermissionMenuEntity taskMenu = menu(94L, null, "admin", 1);
|
||||
taskMenu.setColumnKey("admin_shop_data_crawl_tasks");
|
||||
PermissionMenuEntity duplicateCheckMenu = menu(103L, null, "admin", 2);
|
||||
duplicateCheckMenu.setColumnKey("admin_shop_data_duplicate_check");
|
||||
PermissionMenuEntity dataPermission = menu(76L, null, "internal", 0);
|
||||
dataPermission.setColumnKey("admin_shop_data_crawl_task_data");
|
||||
when(userMapper.selectById(653L)).thenReturn(user(653L, "admin", 1));
|
||||
when(menuMapper.selectOne(any())).thenReturn(taskMenu, duplicateCheckMenu, dataPermission);
|
||||
when(menuMapper.selectById(94L)).thenReturn(taskMenu);
|
||||
when(menuMapper.selectById(103L)).thenReturn(duplicateCheckMenu);
|
||||
when(menuMapper.selectById(76L)).thenReturn(dataPermission);
|
||||
when(menuMapper.selectList(any()))
|
||||
.thenReturn(List.of(taskMenu, duplicateCheckMenu, dataPermission));
|
||||
when(permissionMapper.selectByUserId(653L))
|
||||
.thenReturn(List.of(grant(653L, 103L), grant(653L, 76L)));
|
||||
AdminUserEntity operator = user(653L, "admin", 1);
|
||||
|
||||
service.requireShopDataCrawlTaskAccess(operator);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shopDataTaskAccessAllowsTaskMenuWithoutDuplicateCheckMenu() {
|
||||
// 回归:只勾「店铺数据记录」菜单 + 数据权限,放行。
|
||||
PermissionMenuMapper menuMapper = mock(PermissionMenuMapper.class);
|
||||
UserColumnPermissionMapper permissionMapper = mock(UserColumnPermissionMapper.class);
|
||||
AdminUserMapper userMapper = mock(AdminUserMapper.class);
|
||||
PermissionMenuService service = new PermissionMenuService(menuMapper, permissionMapper, userMapper);
|
||||
|
||||
PermissionMenuEntity taskMenu = menu(94L, null, "admin", 1);
|
||||
taskMenu.setColumnKey("admin_shop_data_crawl_tasks");
|
||||
PermissionMenuEntity duplicateCheckMenu = menu(103L, null, "admin", 2);
|
||||
duplicateCheckMenu.setColumnKey("admin_shop_data_duplicate_check");
|
||||
PermissionMenuEntity dataPermission = menu(76L, null, "internal", 0);
|
||||
dataPermission.setColumnKey("admin_shop_data_crawl_task_data");
|
||||
when(userMapper.selectById(653L)).thenReturn(user(653L, "admin", 1));
|
||||
when(menuMapper.selectOne(any())).thenReturn(taskMenu, duplicateCheckMenu, dataPermission);
|
||||
when(menuMapper.selectById(94L)).thenReturn(taskMenu);
|
||||
when(menuMapper.selectById(103L)).thenReturn(duplicateCheckMenu);
|
||||
when(menuMapper.selectById(76L)).thenReturn(dataPermission);
|
||||
when(menuMapper.selectList(any()))
|
||||
.thenReturn(List.of(taskMenu, duplicateCheckMenu, dataPermission));
|
||||
when(permissionMapper.selectByUserId(653L))
|
||||
.thenReturn(List.of(grant(653L, 94L), grant(653L, 76L)));
|
||||
AdminUserEntity operator = user(653L, "admin", 1);
|
||||
|
||||
service.requireShopDataCrawlTaskAccess(operator);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shopDataTaskAccessRejectsWithoutDataPermission() {
|
||||
// 有店铺数据菜单但缺内部数据权限 → 仍需 403(数据权限走超管「数据范围授权」)。
|
||||
PermissionMenuMapper menuMapper = mock(PermissionMenuMapper.class);
|
||||
UserColumnPermissionMapper permissionMapper = mock(UserColumnPermissionMapper.class);
|
||||
AdminUserMapper userMapper = mock(AdminUserMapper.class);
|
||||
PermissionMenuService service = new PermissionMenuService(menuMapper, permissionMapper, userMapper);
|
||||
|
||||
PermissionMenuEntity taskMenu = menu(94L, null, "admin", 1);
|
||||
taskMenu.setColumnKey("admin_shop_data_crawl_tasks");
|
||||
PermissionMenuEntity duplicateCheckMenu = menu(103L, null, "admin", 2);
|
||||
duplicateCheckMenu.setColumnKey("admin_shop_data_duplicate_check");
|
||||
PermissionMenuEntity dataPermission = menu(76L, null, "internal", 0);
|
||||
dataPermission.setColumnKey("admin_shop_data_crawl_task_data");
|
||||
when(userMapper.selectById(653L)).thenReturn(user(653L, "admin", 1));
|
||||
when(menuMapper.selectOne(any())).thenReturn(taskMenu, duplicateCheckMenu, dataPermission);
|
||||
when(menuMapper.selectById(94L)).thenReturn(taskMenu);
|
||||
when(menuMapper.selectById(103L)).thenReturn(duplicateCheckMenu);
|
||||
when(menuMapper.selectById(76L)).thenReturn(dataPermission);
|
||||
when(menuMapper.selectList(any()))
|
||||
.thenReturn(List.of(taskMenu, duplicateCheckMenu, dataPermission));
|
||||
when(permissionMapper.selectByUserId(653L)).thenReturn(List.of(grant(653L, 103L)));
|
||||
AdminUserEntity operator = user(653L, "admin", 1);
|
||||
|
||||
assertThatThrownBy(() -> service.requireShopDataCrawlTaskAccess(operator))
|
||||
.isInstanceOf(BusinessException.class)
|
||||
.hasMessage("无权查看店铺数据任务");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shopDataTaskAccessRejectsWithoutAnyShopDataMenu() {
|
||||
// 两个店铺数据菜单都没有 → 403。
|
||||
PermissionMenuMapper menuMapper = mock(PermissionMenuMapper.class);
|
||||
UserColumnPermissionMapper permissionMapper = mock(UserColumnPermissionMapper.class);
|
||||
AdminUserMapper userMapper = mock(AdminUserMapper.class);
|
||||
PermissionMenuService service = new PermissionMenuService(menuMapper, permissionMapper, userMapper);
|
||||
|
||||
PermissionMenuEntity taskMenu = menu(94L, null, "admin", 1);
|
||||
taskMenu.setColumnKey("admin_shop_data_crawl_tasks");
|
||||
PermissionMenuEntity duplicateCheckMenu = menu(103L, null, "admin", 2);
|
||||
duplicateCheckMenu.setColumnKey("admin_shop_data_duplicate_check");
|
||||
PermissionMenuEntity dataPermission = menu(76L, null, "internal", 0);
|
||||
dataPermission.setColumnKey("admin_shop_data_crawl_task_data");
|
||||
when(userMapper.selectById(653L)).thenReturn(user(653L, "admin", 1));
|
||||
when(menuMapper.selectOne(any())).thenReturn(taskMenu, duplicateCheckMenu, dataPermission);
|
||||
when(menuMapper.selectById(94L)).thenReturn(taskMenu);
|
||||
when(menuMapper.selectById(103L)).thenReturn(duplicateCheckMenu);
|
||||
when(menuMapper.selectById(76L)).thenReturn(dataPermission);
|
||||
when(menuMapper.selectList(any()))
|
||||
.thenReturn(List.of(taskMenu, duplicateCheckMenu, dataPermission));
|
||||
when(permissionMapper.selectByUserId(653L)).thenReturn(List.of(grant(653L, 5L)));
|
||||
AdminUserEntity operator = user(653L, "admin", 1);
|
||||
|
||||
assertThatThrownBy(() -> service.requireShopDataCrawlTaskAccess(operator))
|
||||
.isInstanceOf(BusinessException.class)
|
||||
.hasMessage("无权查看店铺数据任务");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shopDataTaskAccessAllowsSuperAdminWithoutAnyGrant() {
|
||||
// 超管直通,不依赖任何菜单/数据授权记录。
|
||||
PermissionMenuMapper menuMapper = mock(PermissionMenuMapper.class);
|
||||
UserColumnPermissionMapper permissionMapper = mock(UserColumnPermissionMapper.class);
|
||||
AdminUserMapper userMapper = mock(AdminUserMapper.class);
|
||||
PermissionMenuService service = new PermissionMenuService(menuMapper, permissionMapper, userMapper);
|
||||
|
||||
service.requireShopDataCrawlTaskAccess(user(1L, "super_admin", 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void adminScopedReplacementKeepsExistingShopDataPermissionWithoutDuplicate() {
|
||||
// 用户已通过超管「数据范围授权」配过数据权限时不重复插入。
|
||||
|
||||
Reference in New Issue
Block a user