fix(密钥管理): 分组口径改为「分组创建人名下用户」
- 生产分组成员表 biz_shop_manage_group_member 基本未使用(仅 2 条老数据),导致分组列恒空 - 实际归属:分组 created_by_id/user_id 即主管,其组员为 users.created_by_id 指向该主管的用户 - 分组名批量查询与按分组圈人 SQL 均按此关系重写;按分组筛选含组长本人 - 无分组记录的用户分组列回退展示「所属主管(未建分组)」,避免空值
This commit is contained in:
@@ -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">
|
||||
|
||||
+21
-5
@@ -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
|
||||
|
||||
+3
@@ -19,6 +19,9 @@ public class AdminUserSecretRowVo {
|
||||
@Schema(description = "所属分组名(数据权限分组,可能多个)")
|
||||
private List<String> groups;
|
||||
|
||||
@Schema(description = "所属主管(创建人)用户名;无分组时的兜底展示")
|
||||
private String leaderUsername;
|
||||
|
||||
@Schema(description = "货源查询密钥")
|
||||
private AdminUserSecretModuleVo similarAsin;
|
||||
|
||||
|
||||
+82
-33
@@ -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);
|
||||
}
|
||||
|
||||
/** 主管带的分组 ID(created_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, ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+38
-7
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user