fix(密钥管理): 分组口径改为「分组创建人名下用户」

- 生产分组成员表 biz_shop_manage_group_member 基本未使用(仅 2 条老数据),导致分组列恒空
- 实际归属:分组 created_by_id/user_id 即主管,其组员为 users.created_by_id 指向该主管的用户
- 分组名批量查询与按分组圈人 SQL 均按此关系重写;按分组筛选含组长本人
- 无分组记录的用户分组列回退展示「所属主管(未建分组)」,避免空值
This commit is contained in:
2026-09-13 13:42:06 +08:00
parent 1b733b23b9
commit 0323f0bb7b
6 changed files with 155 additions and 47 deletions
@@ -21,6 +21,8 @@ export interface AdminUserSecretRow {
username: string username: string
/** 所属数据权限分组名(可能多个)。 */ /** 所属数据权限分组名(可能多个)。 */
groups: string[] groups: string[]
/** 所属主管(创建人)用户名;无分组时的兜底展示。 */
leaderUsername: string
similarAsin: AdminUserSecretModule similarAsin: AdminUserSecretModule
appearancePatent: AdminUserSecretModule appearancePatent: AdminUserSecretModule
proxy: AdminUserSecretModule proxy: AdminUserSecretModule
@@ -82,6 +82,13 @@ function moduleTooltip(module: AdminUserSecretModule | undefined) {
return parts.join('') || '暂无检测记录' return parts.join('') || '暂无检测记录'
} }
/** 分组列文案:优先分组名;未建分组的用户回退展示所属主管,便于判断归属。 */
function groupTextOf(row: AdminUserSecretRow) {
if (row.groups?.length) return row.groups.join('、')
if (row.leaderUsername) return `${row.leaderUsername}(未建分组)`
return '—'
}
function rowStatusTooltip(row: AdminUserSecretRow) { function rowStatusTooltip(row: AdminUserSecretRow) {
const parts: string[] = [] const parts: string[] = []
if (row.statusMessage) parts.push(row.statusMessage) if (row.statusMessage) parts.push(row.statusMessage)
@@ -231,7 +238,7 @@ onMounted(load)
<span class="user-name">{{ row.username || '—' }}</span> <span class="user-name">{{ row.username || '—' }}</span>
</td> </td>
<td> <td>
<span class="creator-name">{{ row.groups?.length ? row.groups.join('、') : '—' }}</span> <span class="creator-name">{{ groupTextOf(row) }}</span>
</td> </td>
<td> <td>
<div class="module-cell"> <div class="module-cell">
@@ -103,13 +103,18 @@ public interface ShopManageGroupMapper extends BaseMapper<ShopManageGroupEntity>
""") """)
List<Long> selectUserIdsByGroupIds(@Param("groupIds") List<Long> groupIds); List<Long> selectUserIdsByGroupIds(@Param("groupIds") List<Long> groupIds);
/** 批量查用户所属分组名(密钥管理列表「分组」列展示用)。 */ /**
* 批量查用户所属分组名:分组归属按「分组的创建人(created_by_id/user_id) = 用户的创建人」判定,
* 即主管名下的子账户归入该主管创建的分组(成员表 biz_shop_manage_group_member 生产上基本未使用)。
*/
@Select(""" @Select("""
<script> <script>
SELECT gm.user_id AS userId, g.group_name AS groupName SELECT u.id AS userId, g.group_name AS groupName
FROM biz_shop_manage_group_member gm FROM users u
INNER JOIN biz_shop_manage_group g ON g.id = gm.group_id INNER JOIN biz_shop_manage_group g
WHERE gm.user_id IN ON g.created_by_id = u.created_by_id OR g.user_id = u.created_by_id
WHERE u.created_by_id IS NOT NULL
AND u.id IN
<foreach collection='userIds' item='userId' open='(' separator=',' close=')'> <foreach collection='userIds' item='userId' open='(' separator=',' close=')'>
#{userId} #{userId}
</foreach> </foreach>
@@ -119,6 +124,17 @@ public interface ShopManageGroupMapper extends BaseMapper<ShopManageGroupEntity>
List<com.nanri.aiimage.modules.shopkey.model.dto.UserGroupRef> selectGroupNamesByUserIds( List<com.nanri.aiimage.modules.shopkey.model.dto.UserGroupRef> selectGroupNamesByUserIds(
@Param("userIds") List<Long> userIds); @Param("userIds") List<Long> userIds);
/** 某分组下的用户:该分组创建人(组长)本人 + 其名下子账户。 */
@Select("""
SELECT u.id AS userId
FROM users u
INNER JOIN biz_shop_manage_group g ON g.id = #{groupId}
WHERE u.id = COALESCE(g.created_by_id, g.user_id)
OR u.created_by_id = COALESCE(g.created_by_id, g.user_id)
ORDER BY u.id ASC
""")
List<Long> selectUserIdsByGroupId(@Param("groupId") Long groupId);
/** 某人作为组长(创建人)的分组(密钥管理等按组隔离场景用)。 */ /** 某人作为组长(创建人)的分组(密钥管理等按组隔离场景用)。 */
@Select(""" @Select("""
SELECT g.id AS id, g.group_name AS groupName SELECT g.id AS id, g.group_name AS groupName
@@ -19,6 +19,9 @@ public class AdminUserSecretRowVo {
@Schema(description = "所属分组名(数据权限分组,可能多个)") @Schema(description = "所属分组名(数据权限分组,可能多个)")
private List<String> groups; private List<String> groups;
@Schema(description = "所属主管(创建人)用户名;无分组时的兜底展示")
private String leaderUsername;
@Schema(description = "货源查询密钥") @Schema(description = "货源查询密钥")
private AdminUserSecretModuleVo similarAsin; private AdminUserSecretModuleVo similarAsin;
@@ -213,41 +213,40 @@ public class UserApiSecretService {
boolean superAdmin = "super_admin".equals(adminAuthSupport.currentRole(operator)); boolean superAdmin = "super_admin".equals(adminAuthSupport.currentRole(operator));
Long requestedGroupId = safeQuery.getGroupId(); Long requestedGroupId = safeQuery.getGroupId();
List<Long> ledGroupIds = superAdmin ? List.of() : listLedGroupIds(operator.getId()); // 组隔离:主管强制锁定自己带的分组(组长本人 + 名下子账户);超管按 group_id 筛选(空 = 全部)。
// 组隔离:主管强制锁定自己带的分组;超管按 group_id 筛选(空 = 全部)。 List<Long> scopedUserIds = null;
List<Long> scopedGroupIds;
if (superAdmin) { if (superAdmin) {
scopedGroupIds = requestedGroupId != null && requestedGroupId > 0 ? List.of(requestedGroupId) : null; if (requestedGroupId != null && requestedGroupId > 0) {
scopedUserIds = adminGroupMapper.selectUserIdsByGroupId(requestedGroupId);
}
} else { } else {
if (ledGroupIds.isEmpty()) { scopedUserIds = resolveLedGroupMemberIds(operator.getId());
log.info("[user-secret] 后台密钥列表:主管未带任何分组,返回空 operatorId={}", operator.getId()); if (scopedUserIds.isEmpty()) {
return emptyPageWithGroups(page, pageSize, List.of()); log.info("[user-secret] 后台密钥列表:主管名下无子账户,返回空 operatorId={}", operator.getId());
return emptyPageWithGroups(page, pageSize, groupOptions(operator, false));
} }
scopedGroupIds = ledGroupIds;
} }
List<Long> ledGroupIds = superAdmin ? List.of() : listLedGroupIds(operator.getId());
LambdaQueryWrapper<UserApiSecretEntity> wrapper = new LambdaQueryWrapper<>(); LambdaQueryWrapper<UserApiSecretEntity> wrapper = new LambdaQueryWrapper<>();
String keyword = normalize(safeQuery.getKeyword()); String keyword = normalize(safeQuery.getKeyword());
if (!keyword.isEmpty() || scopedGroupIds != null) { if (!keyword.isEmpty() || scopedUserIds != null) {
// 分组圈定成员用户(含组长本人);再叠加用户名关键字。 List<Long> allowedUserIds = scopedUserIds == null ? null : new ArrayList<>(scopedUserIds);
List<Long> allowedUserIds = scopedGroupIds == null
? new ArrayList<>()
: new ArrayList<>(adminGroupMapper.selectUserIdsByGroupIds(scopedGroupIds));
if (!keyword.isEmpty()) { if (!keyword.isEmpty()) {
List<Long> matched = adminUserMapper.selectList(new LambdaQueryWrapper<AdminUserEntity>() List<Long> matched = adminUserMapper.selectList(new LambdaQueryWrapper<AdminUserEntity>()
.like(AdminUserEntity::getUsername, keyword) .like(AdminUserEntity::getUsername, keyword)
.last("limit 200")) .last("limit 200"))
.stream().map(AdminUserEntity::getId).filter(id -> id != null).toList(); .stream().map(AdminUserEntity::getId).filter(id -> id != null).toList();
if (scopedGroupIds == null) { if (allowedUserIds == null) {
allowedUserIds = new ArrayList<>(matched); allowedUserIds = new ArrayList<>(matched);
} else { } else {
allowedUserIds.retainAll(matched); allowedUserIds.retainAll(matched);
} }
} }
if (allowedUserIds.isEmpty()) { if (allowedUserIds == null || allowedUserIds.isEmpty()) {
log.info("[user-secret] 后台密钥列表无匹配用户 keyword={} groupIds={} operatorId={} role={}", log.info("[user-secret] 后台密钥列表无匹配用户 keyword={} groupId={} operatorId={} role={}",
keyword, scopedGroupIds, operator.getId(), superAdmin ? "super_admin" : "admin"); keyword, requestedGroupId, operator.getId(), superAdmin ? "super_admin" : "admin");
return emptyPageWithGroups(page, pageSize, groupOptions(operator, superAdmin, ledGroupIds)); return emptyPageWithGroups(page, pageSize, groupOptions(operator, superAdmin));
} }
wrapper.in(UserApiSecretEntity::getUserId, allowedUserIds); wrapper.in(UserApiSecretEntity::getUserId, allowedUserIds);
} }
@@ -284,12 +283,37 @@ public class UserApiSecretService {
vo.setTotal(total); vo.setTotal(total);
vo.setPage(page); vo.setPage(page);
vo.setPageSize(pageSize); vo.setPageSize(pageSize);
vo.setGroupOptions(groupOptions(operator, superAdmin, ledGroupIds)); vo.setGroupOptions(groupOptions(operator, superAdmin));
log.info("[user-secret] 后台密钥列表 keyword={} statusFilter={} groupIds={} role={} 聚合用户数={} 本页返回={}", log.info("[user-secret] 后台密钥列表 keyword={} statusFilter={} groupId={} role={} 聚合用户数={} 本页返回={}",
keyword, statusFilter, scopedGroupIds, superAdmin ? "super_admin" : "admin", total, pageItems.size()); keyword, statusFilter, requestedGroupId, superAdmin ? "super_admin" : "admin", total, pageItems.size());
return vo; return vo;
} }
/** 主管可见用户:自己 + 自己带的分组下的子账户(名下 users.created_by_id=自己)。 */
private List<Long> resolveLedGroupMemberIds(Long operatorId) {
if (operatorId == null) {
return List.of();
}
List<Long> ledGroupIds = listLedGroupIds(operatorId);
Set<Long> userIds = new LinkedHashSet<>();
for (Long groupId : ledGroupIds) {
userIds.addAll(adminGroupMapper.selectUserIdsByGroupId(groupId));
}
// 没有分组记录的主管(历史数据)回退按「名下子账户」兜底,避免整个页面空白。
if (userIds.isEmpty()) {
adminUserMapper.selectList(new LambdaQueryWrapper<AdminUserEntity>()
.eq(AdminUserEntity::getCreatedById, operatorId)
.last("limit 2000"))
.forEach(user -> {
if (user.getId() != null) {
userIds.add(user.getId());
}
});
}
userIds.add(operatorId);
return new ArrayList<>(userIds);
}
/** 主管带的分组 IDcreated_by_id / user_id = 自己)。 */ /** 主管带的分组 IDcreated_by_id / user_id = 自己)。 */
private List<Long> listLedGroupIds(Long operatorId) { private List<Long> listLedGroupIds(Long operatorId) {
if (operatorId == null) { if (operatorId == null) {
@@ -301,24 +325,18 @@ public class UserApiSecretService {
.toList(); .toList();
} }
/** 分组筛选项:超管=全部;主管=自己带的分组(没有则不展示下拉)。 */ /** 分组筛选项:超管=全部;主管=自己带的分组。 */
private List<AdminUserSecretPageVo.GroupOptionVo> groupOptions( private List<AdminUserSecretPageVo.GroupOptionVo> groupOptions(AdminUserEntity operator, boolean superAdmin) {
AdminUserEntity operator, boolean superAdmin, List<Long> ledGroupIds) { List<ShopManageGroupEntity> groups = superAdmin
List<ShopManageGroupEntity> groups; ? adminGroupMapper.selectAllGroups()
if (superAdmin) { : adminGroupMapper.selectLedGroups(operator.getId());
groups = adminGroupMapper.selectAllGroups();
} else if (ledGroupIds.isEmpty()) {
return List.of();
} else {
groups = adminGroupMapper.selectLedGroups(operator.getId());
}
return groups.stream() return groups.stream()
.filter(group -> group.getId() != null) .filter(group -> group.getId() != null)
.map(group -> new AdminUserSecretPageVo.GroupOptionVo(group.getId(), group.getGroupName())) .map(group -> new AdminUserSecretPageVo.GroupOptionVo(group.getId(), group.getGroupName()))
.toList(); .toList();
} }
/** 本页行补「所属分组」:一次批量查询,避免逐行查库。 */ /** 本页行补「分组」:一次批量查询;无分组记录的用户回退展示所属主管用户名。 */
private void fillRowGroups(List<AdminUserSecretRowVo> pageItems) { private void fillRowGroups(List<AdminUserSecretRowVo> pageItems) {
List<Long> userIds = pageItems.stream() List<Long> userIds = pageItems.stream()
.map(AdminUserSecretRowVo::getUserId) .map(AdminUserSecretRowVo::getUserId)
@@ -338,8 +356,39 @@ public class UserApiSecretService {
} }
byUser.computeIfAbsent(ref.getUserId(), key -> new ArrayList<>()).add(name); byUser.computeIfAbsent(ref.getUserId(), key -> new ArrayList<>()).add(name);
} }
// 无分组行的兜底:批量取「用户→创建人」,再批量取创建人用户名(全程两次查询)。
Map<Long, Long> userToLeader = new LinkedHashMap<>();
Set<Long> leaderIds = new LinkedHashSet<>();
Map<Long, String> leadersOfEmpty = new LinkedHashMap<>();
for (AdminUserSecretRowVo item : pageItems) { for (AdminUserSecretRowVo item : pageItems) {
item.setGroups(byUser.getOrDefault(item.getUserId(), List.of())); item.setGroups(byUser.getOrDefault(item.getUserId(), List.of()));
if (!item.getGroups().isEmpty()) {
continue;
}
leadersOfEmpty.put(item.getUserId(), "");
}
if (!leadersOfEmpty.isEmpty()) {
for (AdminUserEntity user : adminUserMapper.selectBatchIds(leadersOfEmpty.keySet())) {
if (user.getId() != null && user.getCreatedById() != null) {
userToLeader.put(user.getId(), user.getCreatedById());
leaderIds.add(user.getCreatedById());
}
}
}
if (leaderIds.isEmpty()) {
return;
}
Map<Long, String> leaderNames = new HashMap<>();
for (AdminUserEntity leader : adminUserMapper.selectBatchIds(leaderIds)) {
if (leader.getId() != null) {
leaderNames.put(leader.getId(), normalize(leader.getUsername()));
}
}
for (AdminUserSecretRowVo item : pageItems) {
Long leaderId = userToLeader.get(item.getUserId());
if (leaderId != null) {
item.setLeaderUsername(leaderNames.getOrDefault(leaderId, ""));
}
} }
} }
@@ -249,13 +249,14 @@ class UserApiSecretServiceTest {
UserApiSecretService service = newService(); UserApiSecretService service = newService();
AdminUserEntity operator = new AdminUserEntity(); AdminUserEntity operator = new AdminUserEntity();
operator.setId(88L); operator.setId(88L);
// 主管未带任何分组 → 直接空页,不查密钥表 // 主管既无分组、也名下无子账户 → 只剩自己一行(无密钥记录则空页)
when(adminGroupMapper.selectLedGroups(88L)).thenReturn(List.of()); when(adminGroupMapper.selectLedGroups(88L)).thenReturn(List.of());
when(adminUserMapper.selectList(any())).thenReturn(List.of());
var page = service.adminPage(operator, new AdminUserSecretQuery()); var page = service.adminPage(operator, new AdminUserSecretQuery());
assertThat(page.getItems()).isEmpty(); assertThat(page.getItems()).isEmpty();
verify(mapper, never()).selectList(any()); assertThat(page.getGroupOptions()).isEmpty();
} }
@Test @Test
@@ -267,20 +268,50 @@ class UserApiSecretServiceTest {
group.setId(5L); group.setId(5L);
group.setGroupName("一组"); group.setGroupName("一组");
when(adminGroupMapper.selectLedGroups(88L)).thenReturn(List.of(group)); when(adminGroupMapper.selectLedGroups(88L)).thenReturn(List.of(group));
when(adminGroupMapper.selectUserIdsByGroupIds(any())).thenReturn(List.of(1L)); when(adminGroupMapper.selectUserIdsByGroupId(5L)).thenReturn(List.of(1L, 88L));
when(mapper.selectList(any())).thenReturn(List.of(row(1L, "similar-asin", "enc:sk-1", "passed"))); when(mapper.selectList(any())).thenReturn(List.of(row(1L, "similar-asin", "enc:sk-1", "passed")));
AdminUserEntity user = new AdminUserEntity(); AdminUserEntity user = new AdminUserEntity();
user.setId(1L); user.setId(1L);
user.setUsername("张三"); user.setUsername("张三");
when(adminUserMapper.selectById(1L)).thenReturn(user); when(adminUserMapper.selectById(1L)).thenReturn(user);
when(adminGroupMapper.selectGroupNamesByUserIds(any())).thenReturn(List.of()); UserGroupRef ref = new UserGroupRef();
ref.setUserId(1L);
ref.setGroupName("一组");
when(adminGroupMapper.selectGroupNamesByUserIds(any())).thenReturn(List.of(ref));
var page = service.adminPage(operator, new AdminUserSecretQuery()); var page = service.adminPage(operator, new AdminUserSecretQuery());
assertThat(page.getItems()).hasSize(1); assertThat(page.getItems()).hasSize(1);
assertThat(page.getItems().get(0).getGroups()).containsExactly("一组");
assertThat(page.getGroupOptions()).extracting(com.nanri.aiimage.modules.usersecret.model.vo.AdminUserSecretPageVo.GroupOptionVo::groupName) assertThat(page.getGroupOptions()).extracting(com.nanri.aiimage.modules.usersecret.model.vo.AdminUserSecretPageVo.GroupOptionVo::groupName)
.containsExactly("一组"); .containsExactly("一组");
verify(adminGroupMapper).selectUserIdsByGroupIds(any()); verify(adminGroupMapper).selectUserIdsByGroupId(5L);
}
@Test
void adminPageFallsBackToLeaderUsernameWhenNoGroup() {
UserApiSecretService service = newService();
AdminUserEntity operator = new AdminUserEntity();
operator.setId(88L);
when(adminAuthSupport.currentRole(operator)).thenReturn("super_admin");
when(mapper.selectList(any())).thenReturn(List.of(row(1L, "similar-asin", "enc:sk-1", "passed")));
AdminUserEntity user = new AdminUserEntity();
user.setId(1L);
user.setUsername("张三");
user.setCreatedById(99L);
when(adminUserMapper.selectById(1L)).thenReturn(user);
when(adminGroupMapper.selectGroupNamesByUserIds(any())).thenReturn(List.of());
AdminUserEntity leader = new AdminUserEntity();
leader.setId(99L);
leader.setUsername("主管甲");
when(adminUserMapper.selectBatchIds(any())).thenReturn(List.of(user, leader));
when(adminGroupMapper.selectAllGroups()).thenReturn(List.of());
var page = service.adminPage(operator, new AdminUserSecretQuery());
assertThat(page.getItems()).hasSize(1);
assertThat(page.getItems().get(0).getGroups()).isEmpty();
assertThat(page.getItems().get(0).getLeaderUsername()).isEqualTo("主管甲");
} }
@Test @Test
@@ -289,7 +320,7 @@ class UserApiSecretServiceTest {
AdminUserEntity operator = new AdminUserEntity(); AdminUserEntity operator = new AdminUserEntity();
operator.setId(88L); operator.setId(88L);
when(adminAuthSupport.currentRole(operator)).thenReturn("super_admin"); when(adminAuthSupport.currentRole(operator)).thenReturn("super_admin");
when(adminGroupMapper.selectUserIdsByGroupIds(List.of(5L))).thenReturn(List.of()); when(adminGroupMapper.selectUserIdsByGroupId(5L)).thenReturn(List.of());
when(adminGroupMapper.selectAllGroups()).thenReturn(List.of()); when(adminGroupMapper.selectAllGroups()).thenReturn(List.of());
AdminUserSecretQuery query = new AdminUserSecretQuery(); AdminUserSecretQuery query = new AdminUserSecretQuery();
@@ -297,7 +328,7 @@ class UserApiSecretServiceTest {
var page = service.adminPage(operator, query); var page = service.adminPage(operator, query);
assertThat(page.getItems()).isEmpty(); assertThat(page.getItems()).isEmpty();
verify(adminGroupMapper).selectUserIdsByGroupIds(List.of(5L)); verify(adminGroupMapper).selectUserIdsByGroupId(5L);
} }
@Test @Test