feat(密钥管理): 按数据权限分组隔离与筛选

- 后台密钥列表改为按 biz_shop_manage_group 分组:主管只看自己带的分组成员,超管看全量可按 group_id 筛选
- 行数据补「所属分组」名称(批量查询);列表接口带回分组筛选项
- 修复超管筛选不生效:前端筛选参数统一 snake_case(camel 被后端静默忽略)
- 列表列/筛选项文案由「所属管理员」改为「分组」
This commit is contained in:
2026-09-13 13:34:08 +08:00
parent 05ae0c62d6
commit 1b733b23b9
10 changed files with 251 additions and 96 deletions
@@ -4,6 +4,8 @@ import com.nanri.aiimage.common.security.ShopCredentialCryptoService;
import com.nanri.aiimage.modules.admin.support.AdminAuthSupport;
import com.nanri.aiimage.modules.permission.mapper.AdminUserMapper;
import com.nanri.aiimage.modules.permission.model.entity.AdminUserEntity;
import com.nanri.aiimage.modules.shopkey.model.dto.UserGroupRef;
import com.nanri.aiimage.modules.shopkey.model.entity.ShopManageGroupEntity;
import com.nanri.aiimage.modules.usersecret.client.JikipProxyClient;
import com.nanri.aiimage.modules.usersecret.mapper.UserApiSecretMapper;
import com.nanri.aiimage.modules.usersecret.model.dto.AdminUserSecretQuery;
@@ -32,6 +34,8 @@ class UserApiSecretServiceTest {
private final JikipProxyClient jikipProxyClient = mock(JikipProxyClient.class);
private final AdminUserMapper adminUserMapper = mock(AdminUserMapper.class);
private final AdminAuthSupport adminAuthSupport = mock(AdminAuthSupport.class);
private final com.nanri.aiimage.modules.shopkey.mapper.ShopManageGroupMapper adminGroupMapper =
mock(com.nanri.aiimage.modules.shopkey.mapper.ShopManageGroupMapper.class);
private UserApiSecretService newService() {
when(crypto.encrypt(anyString())).thenAnswer(inv -> "enc:" + inv.getArgument(0, String.class));
@@ -41,7 +45,8 @@ class UserApiSecretServiceTest {
});
// 默认按主管(admin)判定;超管用例里单独改打桩。
when(adminAuthSupport.currentRole(any())).thenReturn("admin");
return new UserApiSecretService(mapper, crypto, checkService, jikipProxyClient, adminUserMapper, adminAuthSupport);
return new UserApiSecretService(
mapper, crypto, checkService, jikipProxyClient, adminUserMapper, adminAuthSupport, adminGroupMapper);
}
@Test
@@ -210,8 +215,6 @@ class UserApiSecretServiceTest {
@Test
void adminPageAggregatesOneRowPerUserWithProxyMasked() {
UserApiSecretService service = newService();
// 非限定查询:无 keyword 无组过滤时不触发 users 表圈定查询。
when(adminUserMapper.selectList(any())).thenReturn(List.of());
when(mapper.selectList(any())).thenReturn(List.of(
row(1L, "similar-asin", "enc:sk-sa-1234", "passed"),
row(1L, "appearance-patent", "enc:sk-ap-5678", "passed"),
@@ -220,11 +223,11 @@ class UserApiSecretServiceTest {
user.setId(1L);
user.setUsername("张三");
when(adminUserMapper.selectById(1L)).thenReturn(user);
AdminUserEntity creator = new AdminUserEntity();
creator.setId(99L);
creator.setUsername("主管甲");
when(adminUserMapper.selectById(99L)).thenReturn(creator);
user.setCreatedById(99L);
// 分组列:用户 1 属于「一组」
UserGroupRef ref = new UserGroupRef();
ref.setUserId(1L);
ref.setGroupName("一组");
when(adminGroupMapper.selectGroupNamesByUserIds(any())).thenReturn(List.of(ref));
AdminUserEntity operator = new AdminUserEntity();
operator.setId(88L);
@@ -234,8 +237,7 @@ class UserApiSecretServiceTest {
assertThat(page.getItems()).hasSize(1);
var rowVo = page.getItems().get(0);
assertThat(rowVo.getUsername()).isEqualTo("张三");
assertThat(rowVo.getCreatedById()).isEqualTo(99L);
assertThat(rowVo.getCreatedByUsername()).isEqualTo("主管甲");
assertThat(rowVo.getGroups()).containsExactly("一组");
assertThat(rowVo.getStatus()).isEqualTo("failed");
assertThat(rowVo.getSimilarAsin().getMasked()).isEqualTo("sk-s****1234");
assertThat(rowVo.getProxy().getExists()).isTrue();
@@ -243,20 +245,61 @@ class UserApiSecretServiceTest {
}
@Test
void adminPageLocksAdminToOwnGroupUsers() {
void adminPageLocksAdminToOwnLedGroups() {
UserApiSecretService service = newService();
// 主管无论传什么 createdById,都强制锁定为本组(created_by_id=88L)。
AdminUserEntity operator = new AdminUserEntity();
operator.setId(88L);
when(adminUserMapper.selectList(any())).thenReturn(List.of());
// 主管未带任何分组 → 直接空页,不查密钥表。
when(adminGroupMapper.selectLedGroups(88L)).thenReturn(List.of());
service.adminPage(operator, new AdminUserSecretQuery());
var page = service.adminPage(operator, new AdminUserSecretQuery());
// 主管模式:先圈定 users 表(组过滤),密钥表因无匹配行不再查询。
verify(adminUserMapper).selectList(any());
assertThat(page.getItems()).isEmpty();
verify(mapper, never()).selectList(any());
}
@Test
void adminPageScopesToLedGroupMembers() {
UserApiSecretService service = newService();
AdminUserEntity operator = new AdminUserEntity();
operator.setId(88L);
ShopManageGroupEntity group = new ShopManageGroupEntity();
group.setId(5L);
group.setGroupName("一组");
when(adminGroupMapper.selectLedGroups(88L)).thenReturn(List.of(group));
when(adminGroupMapper.selectUserIdsByGroupIds(any())).thenReturn(List.of(1L));
when(mapper.selectList(any())).thenReturn(List.of(row(1L, "similar-asin", "enc:sk-1", "passed")));
AdminUserEntity user = new AdminUserEntity();
user.setId(1L);
user.setUsername("张三");
when(adminUserMapper.selectById(1L)).thenReturn(user);
when(adminGroupMapper.selectGroupNamesByUserIds(any())).thenReturn(List.of());
var page = service.adminPage(operator, new AdminUserSecretQuery());
assertThat(page.getItems()).hasSize(1);
assertThat(page.getGroupOptions()).extracting(com.nanri.aiimage.modules.usersecret.model.vo.AdminUserSecretPageVo.GroupOptionVo::groupName)
.containsExactly("一组");
verify(adminGroupMapper).selectUserIdsByGroupIds(any());
}
@Test
void adminPageFiltersSuperAdminByGroupId() {
UserApiSecretService service = newService();
AdminUserEntity operator = new AdminUserEntity();
operator.setId(88L);
when(adminAuthSupport.currentRole(operator)).thenReturn("super_admin");
when(adminGroupMapper.selectUserIdsByGroupIds(List.of(5L))).thenReturn(List.of());
when(adminGroupMapper.selectAllGroups()).thenReturn(List.of());
AdminUserSecretQuery query = new AdminUserSecretQuery();
query.setGroupId(5L);
var page = service.adminPage(operator, query);
assertThat(page.getItems()).isEmpty();
verify(adminGroupMapper).selectUserIdsByGroupIds(List.of(5L));
}
@Test
void adminClearByUserDeletesAllRowsOfUser() {
UserApiSecretService service = newService();