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
/** 所属数据权限分组名(可能多个)。 */
groups: string[]
/** 所属主管(创建人)用户名;无分组时的兜底展示。 */
leaderUsername: string
similarAsin: AdminUserSecretModule
appearancePatent: AdminUserSecretModule
proxy: AdminUserSecretModule
@@ -82,6 +82,13 @@ function moduleTooltip(module: AdminUserSecretModule | undefined) {
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) {
const parts: string[] = []
if (row.statusMessage) parts.push(row.statusMessage)
@@ -231,7 +238,7 @@ onMounted(load)
<span class="user-name">{{ row.username || '—' }}</span>
</td>
<td>
<span class="creator-name">{{ row.groups?.length ? row.groups.join('、') : '—' }}</span>
<span class="creator-name">{{ groupTextOf(row) }}</span>
</td>
<td>
<div class="module-cell">
@@ -103,13 +103,18 @@ public interface ShopManageGroupMapper extends BaseMapper<ShopManageGroupEntity>
""")
List<Long> selectUserIdsByGroupIds(@Param("groupIds") List<Long> groupIds);
/** 批量查用户所属分组名(密钥管理列表「分组」列展示用)。 */
/**
* 批量查用户所属分组名:分组归属按「分组的创建人(created_by_id/user_id) = 用户的创建人」判定,
* 即主管名下的子账户归入该主管创建的分组(成员表 biz_shop_manage_group_member 生产上基本未使用)。
*/
@Select("""
<script>
SELECT gm.user_id AS userId, g.group_name AS groupName
FROM biz_shop_manage_group_member gm
INNER JOIN biz_shop_manage_group g ON g.id = gm.group_id
WHERE gm.user_id IN
SELECT u.id AS userId, g.group_name AS groupName
FROM users u
INNER JOIN biz_shop_manage_group g
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=')'>
#{userId}
</foreach>
@@ -119,6 +124,17 @@ public interface ShopManageGroupMapper extends BaseMapper<ShopManageGroupEntity>
List<com.nanri.aiimage.modules.shopkey.model.dto.UserGroupRef> selectGroupNamesByUserIds(
@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 g.id AS id, g.group_name AS groupName
@@ -19,6 +19,9 @@ public class AdminUserSecretRowVo {
@Schema(description = "所属分组名(数据权限分组,可能多个)")
private List<String> groups;
@Schema(description = "所属主管(创建人)用户名;无分组时的兜底展示")
private String leaderUsername;
@Schema(description = "货源查询密钥")
private AdminUserSecretModuleVo similarAsin;
@@ -213,41 +213,40 @@ public class UserApiSecretService {
boolean superAdmin = "super_admin".equals(adminAuthSupport.currentRole(operator));
Long requestedGroupId = safeQuery.getGroupId();
List<Long> ledGroupIds = superAdmin ? List.of() : listLedGroupIds(operator.getId());
// 组隔离:主管强制锁定自己带的分组;超管按 group_id 筛选(空 = 全部)。
List<Long> scopedGroupIds;
// 组隔离:主管强制锁定自己带的分组(组长本人 + 名下子账户);超管按 group_id 筛选(空 = 全部)。
List<Long> scopedUserIds = null;
if (superAdmin) {
scopedGroupIds = requestedGroupId != null && requestedGroupId > 0 ? List.of(requestedGroupId) : null;
if (requestedGroupId != null && requestedGroupId > 0) {
scopedUserIds = adminGroupMapper.selectUserIdsByGroupId(requestedGroupId);
}
} else {
if (ledGroupIds.isEmpty()) {
log.info("[user-secret] 后台密钥列表:主管未带任何分组,返回空 operatorId={}", operator.getId());
return emptyPageWithGroups(page, pageSize, List.of());
scopedUserIds = resolveLedGroupMemberIds(operator.getId());
if (scopedUserIds.isEmpty()) {
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<>();
String keyword = normalize(safeQuery.getKeyword());
if (!keyword.isEmpty() || scopedGroupIds != null) {
// 分组圈定成员用户(含组长本人);再叠加用户名关键字。
List<Long> allowedUserIds = scopedGroupIds == null
? new ArrayList<>()
: new ArrayList<>(adminGroupMapper.selectUserIdsByGroupIds(scopedGroupIds));
if (!keyword.isEmpty() || scopedUserIds != null) {
List<Long> allowedUserIds = scopedUserIds == null ? null : new ArrayList<>(scopedUserIds);
if (!keyword.isEmpty()) {
List<Long> matched = adminUserMapper.selectList(new LambdaQueryWrapper<AdminUserEntity>()
.like(AdminUserEntity::getUsername, keyword)
.last("limit 200"))
.stream().map(AdminUserEntity::getId).filter(id -> id != null).toList();
if (scopedGroupIds == null) {
if (allowedUserIds == null) {
allowedUserIds = new ArrayList<>(matched);
} else {
allowedUserIds.retainAll(matched);
}
}
if (allowedUserIds.isEmpty()) {
log.info("[user-secret] 后台密钥列表无匹配用户 keyword={} groupIds={} operatorId={} role={}",
keyword, scopedGroupIds, operator.getId(), superAdmin ? "super_admin" : "admin");
return emptyPageWithGroups(page, pageSize, groupOptions(operator, superAdmin, ledGroupIds));
if (allowedUserIds == null || allowedUserIds.isEmpty()) {
log.info("[user-secret] 后台密钥列表无匹配用户 keyword={} groupId={} operatorId={} role={}",
keyword, requestedGroupId, operator.getId(), superAdmin ? "super_admin" : "admin");
return emptyPageWithGroups(page, pageSize, groupOptions(operator, superAdmin));
}
wrapper.in(UserApiSecretEntity::getUserId, allowedUserIds);
}
@@ -284,12 +283,37 @@ public class UserApiSecretService {
vo.setTotal(total);
vo.setPage(page);
vo.setPageSize(pageSize);
vo.setGroupOptions(groupOptions(operator, superAdmin, ledGroupIds));
log.info("[user-secret] 后台密钥列表 keyword={} statusFilter={} groupIds={} role={} 聚合用户数={} 本页返回={}",
keyword, statusFilter, scopedGroupIds, superAdmin ? "super_admin" : "admin", total, pageItems.size());
vo.setGroupOptions(groupOptions(operator, superAdmin));
log.info("[user-secret] 后台密钥列表 keyword={} statusFilter={} groupId={} role={} 聚合用户数={} 本页返回={}",
keyword, statusFilter, requestedGroupId, superAdmin ? "super_admin" : "admin", total, pageItems.size());
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 = 自己)。 */
private List<Long> listLedGroupIds(Long operatorId) {
if (operatorId == null) {
@@ -301,24 +325,18 @@ public class UserApiSecretService {
.toList();
}
/** 分组筛选项:超管=全部;主管=自己带的分组(没有则不展示下拉)。 */
private List<AdminUserSecretPageVo.GroupOptionVo> groupOptions(
AdminUserEntity operator, boolean superAdmin, List<Long> ledGroupIds) {
List<ShopManageGroupEntity> groups;
if (superAdmin) {
groups = adminGroupMapper.selectAllGroups();
} else if (ledGroupIds.isEmpty()) {
return List.of();
} else {
groups = adminGroupMapper.selectLedGroups(operator.getId());
}
/** 分组筛选项:超管=全部;主管=自己带的分组。 */
private List<AdminUserSecretPageVo.GroupOptionVo> groupOptions(AdminUserEntity operator, boolean superAdmin) {
List<ShopManageGroupEntity> groups = superAdmin
? adminGroupMapper.selectAllGroups()
: adminGroupMapper.selectLedGroups(operator.getId());
return groups.stream()
.filter(group -> group.getId() != null)
.map(group -> new AdminUserSecretPageVo.GroupOptionVo(group.getId(), group.getGroupName()))
.toList();
}
/** 本页行补「所属分组」:一次批量查询,避免逐行查库。 */
/** 本页行补「分组」:一次批量查询;无分组记录的用户回退展示所属主管用户名。 */
private void fillRowGroups(List<AdminUserSecretRowVo> pageItems) {
List<Long> userIds = pageItems.stream()
.map(AdminUserSecretRowVo::getUserId)
@@ -338,8 +356,39 @@ public class UserApiSecretService {
}
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) {
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();
AdminUserEntity operator = new AdminUserEntity();
operator.setId(88L);
// 主管未带任何分组 → 直接空页,不查密钥表
// 主管既无分组、也名下无子账户 → 只剩自己一行(无密钥记录则空页)
when(adminGroupMapper.selectLedGroups(88L)).thenReturn(List.of());
when(adminUserMapper.selectList(any())).thenReturn(List.of());
var page = service.adminPage(operator, new AdminUserSecretQuery());
assertThat(page.getItems()).isEmpty();
verify(mapper, never()).selectList(any());
assertThat(page.getGroupOptions()).isEmpty();
}
@Test
@@ -267,20 +268,50 @@ class UserApiSecretServiceTest {
group.setId(5L);
group.setGroupName("一组");
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")));
AdminUserEntity user = new AdminUserEntity();
user.setId(1L);
user.setUsername("张三");
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());
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)
.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
@@ -289,7 +320,7 @@ class UserApiSecretServiceTest {
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.selectUserIdsByGroupId(5L)).thenReturn(List.of());
when(adminGroupMapper.selectAllGroups()).thenReturn(List.of());
AdminUserSecretQuery query = new AdminUserSecretQuery();
@@ -297,7 +328,7 @@ class UserApiSecretServiceTest {
var page = service.adminPage(operator, query);
assertThat(page.getItems()).isEmpty();
verify(adminGroupMapper).selectUserIdsByGroupIds(List.of(5L));
verify(adminGroupMapper).selectUserIdsByGroupId(5L);
}
@Test