mirror of
https://github.com/iflytek/skillhub.git
synced 2026-09-11 22:51:04 +00:00
feat: display username and email in namespace member management
MemberResponse DTO now includes displayName and email fields. NamespacePortalQueryAppService batch-loads UserAccount data via findByIdIn to avoid N+1 queries. Frontend member table shows username (with userId as fallback subtitle) and email columns instead of raw user IDs.
This commit is contained in:
parent
010c1a4e46
commit
03ac86f19d
11 changed files with 84 additions and 9 deletions
|
|
@ -2,6 +2,7 @@ package com.iflytek.skillhub.dto;
|
|||
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceMember;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRole;
|
||||
import com.iflytek.skillhub.domain.user.UserAccount;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
|
|
@ -9,6 +10,8 @@ public record MemberResponse(
|
|||
Long id,
|
||||
Long namespaceId,
|
||||
String userId,
|
||||
String displayName,
|
||||
String email,
|
||||
NamespaceRole role,
|
||||
Instant createdAt,
|
||||
Instant updatedAt
|
||||
|
|
@ -18,6 +21,21 @@ public record MemberResponse(
|
|||
member.getId(),
|
||||
member.getNamespaceId(),
|
||||
member.getUserId(),
|
||||
null,
|
||||
null,
|
||||
member.getRole(),
|
||||
member.getCreatedAt(),
|
||||
member.getUpdatedAt()
|
||||
);
|
||||
}
|
||||
|
||||
public static MemberResponse from(NamespaceMember member, UserAccount user) {
|
||||
return new MemberResponse(
|
||||
member.getId(),
|
||||
member.getNamespaceId(),
|
||||
member.getUserId(),
|
||||
user != null ? user.getDisplayName() : null,
|
||||
user != null ? user.getEmail() : null,
|
||||
member.getRole(),
|
||||
member.getCreatedAt(),
|
||||
member.getUpdatedAt()
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import com.iflytek.skillhub.domain.namespace.NamespaceMember;
|
|||
import com.iflytek.skillhub.domain.namespace.NamespaceMemberService;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRepository;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceService;
|
||||
import com.iflytek.skillhub.domain.user.UserAccount;
|
||||
import com.iflytek.skillhub.domain.user.UserAccountRepository;
|
||||
import com.iflytek.skillhub.dto.MemberResponse;
|
||||
import com.iflytek.skillhub.dto.MessageResponse;
|
||||
import com.iflytek.skillhub.dto.NamespaceLifecycleRequest;
|
||||
|
|
@ -28,15 +30,18 @@ public class NamespacePortalCommandAppService {
|
|||
private final NamespaceRepository namespaceRepository;
|
||||
private final NamespaceGovernanceService namespaceGovernanceService;
|
||||
private final NamespaceMemberService namespaceMemberService;
|
||||
private final UserAccountRepository userAccountRepository;
|
||||
|
||||
public NamespacePortalCommandAppService(NamespaceService namespaceService,
|
||||
NamespaceRepository namespaceRepository,
|
||||
NamespaceGovernanceService namespaceGovernanceService,
|
||||
NamespaceMemberService namespaceMemberService) {
|
||||
NamespaceMemberService namespaceMemberService,
|
||||
UserAccountRepository userAccountRepository) {
|
||||
this.namespaceService = namespaceService;
|
||||
this.namespaceRepository = namespaceRepository;
|
||||
this.namespaceGovernanceService = namespaceGovernanceService;
|
||||
this.namespaceMemberService = namespaceMemberService;
|
||||
this.userAccountRepository = userAccountRepository;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
|
|
@ -135,7 +140,8 @@ public class NamespacePortalCommandAppService {
|
|||
role,
|
||||
operatorUserId
|
||||
);
|
||||
return MemberResponse.from(member);
|
||||
UserAccount user = userAccountRepository.findById(memberUserId).orElse(null);
|
||||
return MemberResponse.from(member, user);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
|
|
@ -157,7 +163,7 @@ public class NamespacePortalCommandAppService {
|
|||
request.role(),
|
||||
operatorUserId
|
||||
);
|
||||
return MemberResponse.from(member);
|
||||
return MemberResponse.from(member, userAccountRepository.findById(userId).orElse(null));
|
||||
}
|
||||
|
||||
private boolean canCreateNamespace(PlatformPrincipal principal) {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import com.iflytek.skillhub.domain.namespace.NamespaceRepository;
|
|||
import com.iflytek.skillhub.domain.namespace.NamespaceRole;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceService;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceStatus;
|
||||
import com.iflytek.skillhub.domain.user.UserAccount;
|
||||
import com.iflytek.skillhub.domain.user.UserAccountRepository;
|
||||
import com.iflytek.skillhub.dto.MemberResponse;
|
||||
import com.iflytek.skillhub.dto.MyNamespaceResponse;
|
||||
import com.iflytek.skillhub.dto.NamespaceResponse;
|
||||
|
|
@ -15,6 +17,8 @@ import com.iflytek.skillhub.dto.PageResponse;
|
|||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
|
@ -31,15 +35,18 @@ public class NamespacePortalQueryAppService {
|
|||
private final NamespaceService namespaceService;
|
||||
private final NamespaceMemberService namespaceMemberService;
|
||||
private final NamespaceAccessPolicy namespaceAccessPolicy;
|
||||
private final UserAccountRepository userAccountRepository;
|
||||
|
||||
public NamespacePortalQueryAppService(NamespaceRepository namespaceRepository,
|
||||
NamespaceService namespaceService,
|
||||
NamespaceMemberService namespaceMemberService,
|
||||
NamespaceAccessPolicy namespaceAccessPolicy) {
|
||||
NamespaceAccessPolicy namespaceAccessPolicy,
|
||||
UserAccountRepository userAccountRepository) {
|
||||
this.namespaceRepository = namespaceRepository;
|
||||
this.namespaceService = namespaceService;
|
||||
this.namespaceMemberService = namespaceMemberService;
|
||||
this.namespaceAccessPolicy = namespaceAccessPolicy;
|
||||
this.userAccountRepository = userAccountRepository;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
|
|
@ -78,6 +85,18 @@ public class NamespacePortalQueryAppService {
|
|||
Namespace namespace = namespaceService.getNamespaceBySlug(slug);
|
||||
namespaceService.assertMember(namespace.getId(), userId);
|
||||
Page<NamespaceMember> members = namespaceMemberService.listMembers(namespace.getId(), pageable);
|
||||
return PageResponse.from(members.map(MemberResponse::from));
|
||||
|
||||
List<String> memberUserIds = members.getContent().stream()
|
||||
.map(NamespaceMember::getUserId)
|
||||
.toList();
|
||||
|
||||
Map<String, UserAccount> userMap = memberUserIds.isEmpty()
|
||||
? Map.of()
|
||||
: userAccountRepository.findByIdIn(memberUserIds).stream()
|
||||
.collect(Collectors.toMap(UserAccount::getId, Function.identity()));
|
||||
|
||||
return PageResponse.from(members.map(member ->
|
||||
MemberResponse.from(member, userMap.get(member.getUserId()))
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import com.iflytek.skillhub.domain.namespace.NamespaceService;
|
|||
import com.iflytek.skillhub.domain.namespace.NamespaceStatus;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceType;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException;
|
||||
import com.iflytek.skillhub.domain.user.UserAccount;
|
||||
import com.iflytek.skillhub.domain.user.UserAccountRepository;
|
||||
import com.iflytek.skillhub.dto.NamespaceCandidateUserResponse;
|
||||
import com.iflytek.skillhub.service.NamespaceMemberCandidateService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
|
@ -71,6 +73,9 @@ class NamespacePortalControllerTest {
|
|||
@MockBean
|
||||
private DeviceAuthService deviceAuthService;
|
||||
|
||||
@MockBean
|
||||
private UserAccountRepository userAccountRepository;
|
||||
|
||||
@Test
|
||||
void listMyNamespaces_returnsFrozenAndArchivedNamespacesWithCurrentRole() throws Exception {
|
||||
Namespace namespace = namespace(1L, "team-a", NamespaceStatus.ARCHIVED, NamespaceType.TEAM);
|
||||
|
|
@ -188,9 +193,12 @@ class NamespacePortalControllerTest {
|
|||
void addMember_returnsCreatedMember() throws Exception {
|
||||
Namespace namespace = namespace(1L, "team-a", NamespaceStatus.ACTIVE, NamespaceType.TEAM);
|
||||
NamespaceMember member = new NamespaceMember(1L, "user-2", NamespaceRole.ADMIN);
|
||||
UserAccount user = new UserAccount("user-2", "Alice", "alice@example.com", null);
|
||||
given(namespaceService.getNamespaceBySlug("team-a")).willReturn(namespace);
|
||||
given(namespaceMemberService.addMember(1L, "user-2", NamespaceRole.ADMIN, "owner-1"))
|
||||
.willReturn(member);
|
||||
given(userAccountRepository.findById("user-2"))
|
||||
.willReturn(java.util.Optional.of(user));
|
||||
|
||||
mockMvc.perform(post("/api/v1/namespaces/team-a/members")
|
||||
.with(csrf())
|
||||
|
|
@ -224,9 +232,12 @@ class NamespacePortalControllerTest {
|
|||
void updateMemberRole_returnsUpdatedMember() throws Exception {
|
||||
Namespace namespace = namespace(1L, "team-a", NamespaceStatus.ACTIVE, NamespaceType.TEAM);
|
||||
NamespaceMember member = new NamespaceMember(1L, "user-2", NamespaceRole.OWNER);
|
||||
UserAccount user = new UserAccount("user-2", "Alice", "alice@example.com", null);
|
||||
given(namespaceService.getNamespaceBySlug("team-a")).willReturn(namespace);
|
||||
given(namespaceMemberService.updateMemberRole(1L, "user-2", NamespaceRole.OWNER, "owner-1"))
|
||||
.willReturn(member);
|
||||
given(userAccountRepository.findById("user-2"))
|
||||
.willReturn(java.util.Optional.of(user));
|
||||
|
||||
mockMvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put("/api/v1/namespaces/team-a/members/user-2/role")
|
||||
.with(csrf())
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import com.iflytek.skillhub.domain.namespace.NamespaceRepository;
|
|||
import com.iflytek.skillhub.domain.namespace.NamespaceService;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceStatus;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceType;
|
||||
import com.iflytek.skillhub.domain.user.UserAccountRepository;
|
||||
import com.iflytek.skillhub.dto.NamespaceLifecycleRequest;
|
||||
import com.iflytek.skillhub.dto.NamespaceRequest;
|
||||
import com.iflytek.skillhub.exception.ForbiddenException;
|
||||
|
|
@ -28,11 +29,13 @@ class NamespacePortalCommandAppServiceTest {
|
|||
private final NamespaceRepository namespaceRepository = mock(NamespaceRepository.class);
|
||||
private final NamespaceGovernanceService namespaceGovernanceService = mock(NamespaceGovernanceService.class);
|
||||
private final NamespaceMemberService namespaceMemberService = mock(NamespaceMemberService.class);
|
||||
private final UserAccountRepository userAccountRepository = mock(UserAccountRepository.class);
|
||||
private final NamespacePortalCommandAppService service = new NamespacePortalCommandAppService(
|
||||
namespaceService,
|
||||
namespaceRepository,
|
||||
namespaceGovernanceService,
|
||||
namespaceMemberService
|
||||
namespaceMemberService,
|
||||
userAccountRepository
|
||||
);
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import com.iflytek.skillhub.domain.namespace.NamespaceRole;
|
|||
import com.iflytek.skillhub.domain.namespace.NamespaceService;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceStatus;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceType;
|
||||
import com.iflytek.skillhub.domain.user.UserAccountRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
|
|
@ -25,11 +26,13 @@ class NamespacePortalQueryAppServiceTest {
|
|||
private final NamespaceService namespaceService = mock(NamespaceService.class);
|
||||
private final NamespaceMemberService namespaceMemberService = mock(NamespaceMemberService.class);
|
||||
private final NamespaceAccessPolicy namespaceAccessPolicy = mock(NamespaceAccessPolicy.class);
|
||||
private final UserAccountRepository userAccountRepository = mock(UserAccountRepository.class);
|
||||
private final NamespacePortalQueryAppService service = new NamespacePortalQueryAppService(
|
||||
namespaceRepository,
|
||||
namespaceService,
|
||||
namespaceMemberService,
|
||||
namespaceAccessPolicy
|
||||
namespaceAccessPolicy,
|
||||
userAccountRepository
|
||||
);
|
||||
|
||||
@Test
|
||||
|
|
|
|||
2
web/src/api/generated/schema.d.ts
vendored
2
web/src/api/generated/schema.d.ts
vendored
|
|
@ -3037,6 +3037,8 @@ export interface components {
|
|||
/** Format: int64 */
|
||||
namespaceId?: number;
|
||||
userId?: string;
|
||||
displayName?: string;
|
||||
email?: string;
|
||||
/** @enum {string} */
|
||||
role?: "OWNER" | "ADMIN" | "MEMBER";
|
||||
/** Format: date-time */
|
||||
|
|
|
|||
|
|
@ -116,6 +116,8 @@ export interface ManagedNamespace extends Namespace {
|
|||
export interface NamespaceMember {
|
||||
id: number
|
||||
userId: string
|
||||
displayName?: string
|
||||
email?: string
|
||||
role: NamespaceRole
|
||||
createdAt: string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -983,6 +983,8 @@
|
|||
"savingRole": "Saving...",
|
||||
"changeRole": "Change role",
|
||||
"colUserId": "User ID",
|
||||
"colUsername": "Username",
|
||||
"colEmail": "Email",
|
||||
"colRole": "Role",
|
||||
"colJoinedAt": "Joined At",
|
||||
"colActions": "Actions",
|
||||
|
|
|
|||
|
|
@ -983,6 +983,8 @@
|
|||
"savingRole": "保存中...",
|
||||
"changeRole": "修改角色",
|
||||
"colUserId": "用户 ID",
|
||||
"colUsername": "用户名",
|
||||
"colEmail": "邮箱",
|
||||
"colRole": "角色",
|
||||
"colJoinedAt": "加入时间",
|
||||
"colActions": "操作",
|
||||
|
|
|
|||
|
|
@ -178,7 +178,8 @@ export function NamespaceMembersPage() {
|
|||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-border/40">
|
||||
<th className="text-left p-4 font-medium font-heading text-sm text-muted-foreground">{t('members.colUserId')}</th>
|
||||
<th className="text-left p-4 font-medium font-heading text-sm text-muted-foreground">{t('members.colUsername')}</th>
|
||||
<th className="text-left p-4 font-medium font-heading text-sm text-muted-foreground">{t('members.colEmail')}</th>
|
||||
<th className="text-left p-4 font-medium font-heading text-sm text-muted-foreground">{t('members.colRole')}</th>
|
||||
<th className="text-left p-4 font-medium font-heading text-sm text-muted-foreground">{t('members.colJoinedAt')}</th>
|
||||
<th className="text-right p-4 font-medium font-heading text-sm text-muted-foreground">{t('members.colActions')}</th>
|
||||
|
|
@ -193,7 +194,13 @@ export function NamespaceMembersPage() {
|
|||
|
||||
return (
|
||||
<tr key={member.id} className="border-b border-border/40 last:border-b-0 hover:bg-secondary/30 transition-colors">
|
||||
<td className="p-4 font-medium font-mono">{member.userId}</td>
|
||||
<td className="p-4">
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{member.displayName || member.userId}</span>
|
||||
<span className="text-xs text-muted-foreground">{member.userId}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-4 text-sm text-muted-foreground">{member.email || '-'}</td>
|
||||
<td className="p-4">
|
||||
{canManageMembers && !isOwner ? (
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue