diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/ClawHubCompatController.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/ClawHubCompatController.java index 70717170..de278d66 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/ClawHubCompatController.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/ClawHubCompatController.java @@ -1,28 +1,56 @@ package com.iflytek.skillhub.compat; import com.iflytek.skillhub.auth.rbac.PlatformPrincipal; +import com.iflytek.skillhub.compat.dto.ClawHubSkillItem; import com.iflytek.skillhub.compat.dto.ClawHubResolveResponse; import com.iflytek.skillhub.compat.dto.ClawHubSearchResponse; import com.iflytek.skillhub.compat.dto.ClawHubWhoamiResponse; +import com.iflytek.skillhub.domain.namespace.NamespaceRole; +import com.iflytek.skillhub.service.SkillSearchAppService; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import java.util.List; +import java.util.Map; @RestController @RequestMapping("/api/compat/v1") public class ClawHubCompatController { private final CanonicalSlugMapper mapper; + private final SkillSearchAppService skillSearchAppService; - public ClawHubCompatController(CanonicalSlugMapper mapper) { + public ClawHubCompatController(CanonicalSlugMapper mapper, SkillSearchAppService skillSearchAppService) { this.mapper = mapper; + this.skillSearchAppService = skillSearchAppService; } @GetMapping("/search") - public ClawHubSearchResponse search(@RequestParam String q) { - // Return empty results for now (placeholder) - return new ClawHubSearchResponse(List.of()); + public ClawHubSearchResponse search( + @RequestParam String q, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int limit, + @RequestAttribute(value = "userId", required = false) String userId, + @RequestAttribute(value = "userNsRoles", required = false) Map userNsRoles) { + SkillSearchAppService.SearchResponse response = skillSearchAppService.search( + q, + null, + q == null || q.isBlank() ? "newest" : "relevance", + page, + limit, + userId, + userNsRoles + ); + + List items = response.items().stream() + .map(item -> new ClawHubSkillItem( + mapper.toCanonical(item.namespace(), item.slug()), + item.summary(), + item.latestVersion(), + item.starCount())) + .toList(); + + return new ClawHubSearchResponse(items); } @GetMapping("/resolve/{canonicalSlug}") diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/admin/AuditLogController.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/admin/AuditLogController.java index 4426113f..21fa7405 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/admin/AuditLogController.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/admin/AuditLogController.java @@ -5,19 +5,20 @@ import com.iflytek.skillhub.dto.ApiResponse; import com.iflytek.skillhub.dto.ApiResponseFactory; import com.iflytek.skillhub.dto.AuditLogItemResponse; import com.iflytek.skillhub.dto.PageResponse; -import org.springframework.data.domain.PageImpl; +import com.iflytek.skillhub.service.AdminAuditLogAppService; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; -import java.time.Instant; -import java.util.List; - @RestController @RequestMapping("/api/v1/admin/audit-logs") public class AuditLogController extends BaseApiController { - public AuditLogController(ApiResponseFactory responseFactory) { + private final AdminAuditLogAppService adminAuditLogAppService; + + public AuditLogController(AdminAuditLogAppService adminAuditLogAppService, + ApiResponseFactory responseFactory) { super(responseFactory); + this.adminAuditLogAppService = adminAuditLogAppService; } @GetMapping @@ -27,15 +28,6 @@ public class AuditLogController extends BaseApiController { @RequestParam(defaultValue = "20") int size, @RequestParam(required = false) String userId, @RequestParam(required = false) String action) { - List logs = List.of( - new AuditLogItemResponse( - "log-1", "user-1", "CREATE_SKILL", "SKILL", "skill-123", Instant.now(), "192.168.1.1" - ), - new AuditLogItemResponse( - "log-2", "user-2", "UPDATE_NAMESPACE", "NAMESPACE", "ns-456", - Instant.now().minusSeconds(3600), "192.168.1.2" - ) - ); - return ok("response.success.read", PageResponse.from(new PageImpl<>(logs))); + return ok("response.success.read", adminAuditLogAppService.listAuditLogs(page, size, userId, action)); } } diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/admin/UserManagementController.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/admin/UserManagementController.java index 8f4bb657..bffd32c5 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/admin/UserManagementController.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/admin/UserManagementController.java @@ -1,6 +1,7 @@ package com.iflytek.skillhub.controller.admin; import com.iflytek.skillhub.controller.BaseApiController; +import com.iflytek.skillhub.auth.rbac.PlatformPrincipal; import com.iflytek.skillhub.dto.AdminUserMutationResponse; import com.iflytek.skillhub.dto.AdminUserRoleUpdateRequest; import com.iflytek.skillhub.dto.AdminUserStatusUpdateRequest; @@ -8,39 +9,42 @@ import com.iflytek.skillhub.dto.AdminUserSummaryResponse; import com.iflytek.skillhub.dto.ApiResponse; import com.iflytek.skillhub.dto.ApiResponseFactory; import com.iflytek.skillhub.dto.PageResponse; +import com.iflytek.skillhub.service.AdminUserAppService; import jakarta.validation.Valid; -import org.springframework.data.domain.PageImpl; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; -import java.util.List; - @RestController @RequestMapping("/api/v1/admin/users") public class UserManagementController extends BaseApiController { - public UserManagementController(ApiResponseFactory responseFactory) { + private final AdminUserAppService adminUserAppService; + + public UserManagementController(AdminUserAppService adminUserAppService, + ApiResponseFactory responseFactory) { super(responseFactory); + this.adminUserAppService = adminUserAppService; } @GetMapping @PreAuthorize("hasAnyRole('USER_ADMIN', 'SUPER_ADMIN')") public ApiResponse> listUsers( + @RequestParam(required = false) String search, + @RequestParam(required = false) String status, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size) { - List users = List.of( - new AdminUserSummaryResponse("user-1", "alice", "USER", "ACTIVE"), - new AdminUserSummaryResponse("user-2", "bob", "USER", "ACTIVE") - ); - return ok("response.success.read", PageResponse.from(new PageImpl<>(users))); + return ok("response.success.read", adminUserAppService.listUsers(search, status, page, size)); } @PutMapping("/{userId}/role") @PreAuthorize("hasAnyRole('USER_ADMIN', 'SUPER_ADMIN')") public ApiResponse updateUserRole( @PathVariable String userId, + @AuthenticationPrincipal PlatformPrincipal principal, @Valid @RequestBody AdminUserRoleUpdateRequest request) { - return ok("response.success.updated", new AdminUserMutationResponse(userId, request.role(), null)); + return ok("response.success.updated", + adminUserAppService.updateUserRole(userId, request.role(), principal.platformRoles())); } @PutMapping("/{userId}/status") @@ -48,6 +52,6 @@ public class UserManagementController extends BaseApiController { public ApiResponse updateUserStatus( @PathVariable String userId, @Valid @RequestBody AdminUserStatusUpdateRequest request) { - return ok("response.success.updated", new AdminUserMutationResponse(userId, null, request.status())); + return ok("response.success.updated", adminUserAppService.updateUserStatus(userId, request.status())); } } diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/AdminUserSummaryResponse.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/AdminUserSummaryResponse.java index 3d569967..3cdbbfd2 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/AdminUserSummaryResponse.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/AdminUserSummaryResponse.java @@ -1,9 +1,14 @@ package com.iflytek.skillhub.dto; +import java.time.LocalDateTime; +import java.util.List; + public record AdminUserSummaryResponse( - String userId, + String id, String username, - String role, - String status + String email, + String status, + List platformRoles, + LocalDateTime createdAt ) { } diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/AuditLogItemResponse.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/AuditLogItemResponse.java index cd285c8a..699c1b19 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/AuditLogItemResponse.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/AuditLogItemResponse.java @@ -3,12 +3,12 @@ package com.iflytek.skillhub.dto; import java.time.Instant; public record AuditLogItemResponse( - String id, - String userId, + Long id, String action, - String resourceType, - String resourceId, - Instant timestamp, - String ipAddress + String userId, + String username, + String details, + String ipAddress, + Instant timestamp ) { } diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/exception/GlobalExceptionHandler.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/exception/GlobalExceptionHandler.java index c1e91628..49625f63 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/exception/GlobalExceptionHandler.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/exception/GlobalExceptionHandler.java @@ -4,6 +4,7 @@ import com.iflytek.skillhub.dto.ApiResponse; import com.iflytek.skillhub.dto.ApiResponseFactory; import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException; import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException; +import com.iflytek.skillhub.domain.shared.exception.DomainNotFoundException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.MDC; @@ -43,6 +44,12 @@ public class GlobalExceptionHandler { apiResponseFactory.error(403, ex.messageCode(), ex.messageArgs())); } + @ExceptionHandler(DomainNotFoundException.class) + public ResponseEntity> handleDomainNotFound(DomainNotFoundException ex) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body( + apiResponseFactory.error(404, ex.messageCode(), ex.messageArgs())); + } + @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity> handleValidation(MethodArgumentNotValidException ex) { String msg = ex.getBindingResult().getFieldErrors().stream() diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/repository/AdminUserSearchRepository.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/repository/AdminUserSearchRepository.java new file mode 100644 index 00000000..c68bb7ed --- /dev/null +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/repository/AdminUserSearchRepository.java @@ -0,0 +1,74 @@ +package com.iflytek.skillhub.repository; + +import com.iflytek.skillhub.domain.user.UserAccount; +import com.iflytek.skillhub.domain.user.UserStatus; +import jakarta.persistence.EntityManager; +import jakarta.persistence.TypedQuery; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; +import org.springframework.util.StringUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +@Repository +public class AdminUserSearchRepository { + + private final EntityManager entityManager; + + public AdminUserSearchRepository(EntityManager entityManager) { + this.entityManager = entityManager; + } + + public Page search(String search, UserStatus status, Pageable pageable) { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + + CriteriaQuery query = builder.createQuery(UserAccount.class); + Root root = query.from(UserAccount.class); + List predicates = buildPredicates(search, status, builder, root); + query.select(root) + .where(predicates.toArray(Predicate[]::new)) + .orderBy(builder.desc(root.get("createdAt"))); + + TypedQuery typedQuery = entityManager.createQuery(query); + typedQuery.setFirstResult((int) pageable.getOffset()); + typedQuery.setMaxResults(pageable.getPageSize()); + List users = typedQuery.getResultList(); + + CriteriaQuery countQuery = builder.createQuery(Long.class); + Root countRoot = countQuery.from(UserAccount.class); + List countPredicates = buildPredicates(search, status, builder, countRoot); + countQuery.select(builder.count(countRoot)) + .where(countPredicates.toArray(Predicate[]::new)); + long total = entityManager.createQuery(countQuery).getSingleResult(); + + return new PageImpl<>(users, pageable, total); + } + + private List buildPredicates( + String search, + UserStatus status, + CriteriaBuilder builder, + Root root) { + List predicates = new ArrayList<>(); + if (StringUtils.hasText(search)) { + String normalized = "%" + search.trim().toLowerCase(Locale.ROOT) + "%"; + predicates.add(builder.or( + builder.like(builder.lower(root.get("id")), normalized), + builder.like(builder.lower(root.get("displayName")), normalized), + builder.like(builder.lower(root.get("email")), normalized) + )); + } + if (status != null) { + predicates.add(builder.equal(root.get("status"), status)); + } + return predicates; + } +} diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/AdminAuditLogAppService.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/AdminAuditLogAppService.java new file mode 100644 index 00000000..d1673617 --- /dev/null +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/AdminAuditLogAppService.java @@ -0,0 +1,97 @@ +package com.iflytek.skillhub.service; + +import com.iflytek.skillhub.dto.AuditLogItemResponse; +import com.iflytek.skillhub.dto.PageResponse; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.sql.Timestamp; +import java.time.Instant; +import java.util.List; + +@Service +public class AdminAuditLogAppService { + + private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; + + public AdminAuditLogAppService(NamedParameterJdbcTemplate namedParameterJdbcTemplate) { + this.namedParameterJdbcTemplate = namedParameterJdbcTemplate; + } + + @Transactional(readOnly = true) + public PageResponse listAuditLogs(int page, int size, String userId, String action) { + MapSqlParameterSource parameters = new MapSqlParameterSource() + .addValue("limit", size) + .addValue("offset", Math.max(page, 0) * size); + + String whereClause = buildWhereClause(parameters, userId, action); + Long total = namedParameterJdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM audit_log al" + whereClause, + parameters, + Long.class + ); + + List items = namedParameterJdbcTemplate.query( + """ + SELECT al.id, + al.action, + al.actor_user_id, + ua.display_name, + al.detail_json, + al.target_type, + al.target_id, + al.client_ip, + al.created_at + FROM audit_log al + LEFT JOIN user_account ua ON ua.id = al.actor_user_id + """ + whereClause + """ + ORDER BY al.created_at DESC + LIMIT :limit OFFSET :offset + """, + parameters, + (rs, rowNum) -> new AuditLogItemResponse( + rs.getLong("id"), + rs.getString("action"), + rs.getString("actor_user_id"), + rs.getString("display_name"), + renderDetails( + rs.getString("detail_json"), + rs.getString("target_type"), + rs.getObject("target_id")), + rs.getString("client_ip"), + toInstant(rs.getTimestamp("created_at"))) + ); + + return new PageResponse<>(items, total == null ? 0 : total, page, size); + } + + private String buildWhereClause(MapSqlParameterSource parameters, String userId, String action) { + StringBuilder clause = new StringBuilder(" WHERE 1 = 1"); + if (StringUtils.hasText(userId)) { + clause.append(" AND al.actor_user_id = :userId"); + parameters.addValue("userId", userId.trim()); + } + if (StringUtils.hasText(action)) { + clause.append(" AND al.action = :action"); + parameters.addValue("action", action.trim()); + } + return clause.toString(); + } + + private String renderDetails(String detailJson, String targetType, Object targetId) { + if (StringUtils.hasText(detailJson)) { + return detailJson; + } + if (!StringUtils.hasText(targetType) && targetId == null) { + return null; + } + return targetType + ":" + targetId; + } + + private Instant toInstant(Timestamp timestamp) { + return timestamp == null ? null : timestamp.toInstant(); + } +} diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/AdminUserAppService.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/AdminUserAppService.java new file mode 100644 index 00000000..5d7a140d --- /dev/null +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/AdminUserAppService.java @@ -0,0 +1,145 @@ +package com.iflytek.skillhub.service; + +import com.iflytek.skillhub.auth.entity.Role; +import com.iflytek.skillhub.auth.entity.UserRoleBinding; +import com.iflytek.skillhub.auth.repository.RoleRepository; +import com.iflytek.skillhub.auth.repository.UserRoleBindingRepository; +import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException; +import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException; +import com.iflytek.skillhub.domain.shared.exception.DomainNotFoundException; +import com.iflytek.skillhub.domain.user.UserAccount; +import com.iflytek.skillhub.domain.user.UserAccountRepository; +import com.iflytek.skillhub.domain.user.UserStatus; +import com.iflytek.skillhub.dto.AdminUserMutationResponse; +import com.iflytek.skillhub.dto.AdminUserSummaryResponse; +import com.iflytek.skillhub.dto.PageResponse; +import com.iflytek.skillhub.repository.AdminUserSearchRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@Service +public class AdminUserAppService { + + private static final Set MANAGEABLE_STATUSES = Set.of(UserStatus.ACTIVE, UserStatus.DISABLED); + + private final AdminUserSearchRepository adminUserSearchRepository; + private final UserAccountRepository userAccountRepository; + private final UserRoleBindingRepository userRoleBindingRepository; + private final RoleRepository roleRepository; + + public AdminUserAppService( + AdminUserSearchRepository adminUserSearchRepository, + UserAccountRepository userAccountRepository, + UserRoleBindingRepository userRoleBindingRepository, + RoleRepository roleRepository) { + this.adminUserSearchRepository = adminUserSearchRepository; + this.userAccountRepository = userAccountRepository; + this.userRoleBindingRepository = userRoleBindingRepository; + this.roleRepository = roleRepository; + } + + @Transactional(readOnly = true) + public PageResponse listUsers(String search, String status, int page, int size) { + Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt")); + Page result = adminUserSearchRepository.search( + search, + StringUtils.hasText(status) ? parseStatus(status) : null, + pageable + ); + Map> rolesByUserId = loadRolesByUserId( + result.getContent().stream().map(UserAccount::getId).toList()); + + List items = result.getContent().stream() + .map(user -> new AdminUserSummaryResponse( + user.getId(), + user.getDisplayName(), + user.getEmail(), + user.getStatus().name(), + rolesByUserId.getOrDefault(user.getId(), List.of()), + user.getCreatedAt())) + .toList(); + + return new PageResponse<>(items, result.getTotalElements(), result.getNumber(), result.getSize()); + } + + @Transactional + public AdminUserMutationResponse updateUserRole(String userId, String roleCode, Set actorPlatformRoles) { + UserAccount user = loadUser(userId); + String normalizedRoleCode = normalizeRoleCode(roleCode); + + if ("SUPER_ADMIN".equals(normalizedRoleCode) + && (actorPlatformRoles == null || !actorPlatformRoles.contains("SUPER_ADMIN"))) { + throw new DomainForbiddenException("error.admin.user.role.superAdmin.assignDenied"); + } + + userRoleBindingRepository.deleteByUserId(user.getId()); + + if (!"USER".equals(normalizedRoleCode)) { + Role role = roleRepository.findByCode(normalizedRoleCode) + .orElseThrow(() -> new DomainBadRequestException("error.admin.user.role.invalid", roleCode)); + userRoleBindingRepository.save(new UserRoleBinding(user.getId(), role)); + } + + return new AdminUserMutationResponse(user.getId(), normalizedRoleCode, user.getStatus().name()); + } + + @Transactional + public AdminUserMutationResponse updateUserStatus(String userId, String status) { + UserAccount user = loadUser(userId); + UserStatus nextStatus = parseManageableStatus(status); + user.setStatus(nextStatus); + userAccountRepository.save(user); + return new AdminUserMutationResponse(user.getId(), null, nextStatus.name()); + } + + private UserStatus parseManageableStatus(String status) { + UserStatus parsedStatus = parseStatus(status); + if (!MANAGEABLE_STATUSES.contains(parsedStatus)) { + throw new DomainBadRequestException("error.admin.user.status.unsupported"); + } + return parsedStatus; + } + + private UserStatus parseStatus(String status) { + try { + return UserStatus.valueOf(status.trim().toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException ex) { + throw new DomainBadRequestException("error.admin.user.status.invalid", status); + } + } + + private String normalizeRoleCode(String roleCode) { + if (!StringUtils.hasText(roleCode)) { + throw new DomainBadRequestException("error.admin.user.role.invalid", roleCode); + } + return roleCode.trim().toUpperCase(Locale.ROOT); + } + + private Map> loadRolesByUserId(List userIds) { + if (userIds.isEmpty()) { + return Map.of(); + } + return userRoleBindingRepository.findByUserIdIn(userIds).stream() + .collect(Collectors.groupingBy( + UserRoleBinding::getUserId, + Collectors.mapping(binding -> binding.getRole().getCode(), + Collectors.collectingAndThen(Collectors.toList(), + roles -> roles.stream().sorted().toList())))); + } + + private UserAccount loadUser(String userId) { + return userAccountRepository.findById(userId) + .orElseThrow(() -> new DomainNotFoundException("error.admin.user.notFound", userId)); + } +} diff --git a/server/skillhub-app/src/main/resources/messages.properties b/server/skillhub-app/src/main/resources/messages.properties index 7cc2c460..c8b31e6b 100644 --- a/server/skillhub-app/src/main/resources/messages.properties +++ b/server/skillhub-app/src/main/resources/messages.properties @@ -71,3 +71,8 @@ error.deviceAuth.userCode.invalid=Invalid or expired user code error.deviceAuth.deviceCode.expired=Device code expired error.deviceAuth.deviceCode.invalid=Device code expired or invalid error.deviceAuth.deviceCode.used=Device code has already been used +error.admin.user.notFound=User not found: {0} +error.admin.user.role.invalid=Invalid role: {0} +error.admin.user.role.superAdmin.assignDenied=Only SUPER_ADMIN can assign SUPER_ADMIN role +error.admin.user.status.invalid=Invalid user status: {0} +error.admin.user.status.unsupported=Only ACTIVE or DISABLED status can be managed here diff --git a/server/skillhub-app/src/main/resources/messages_zh.properties b/server/skillhub-app/src/main/resources/messages_zh.properties index 5192afd1..9480c9df 100644 --- a/server/skillhub-app/src/main/resources/messages_zh.properties +++ b/server/skillhub-app/src/main/resources/messages_zh.properties @@ -71,3 +71,8 @@ error.deviceAuth.userCode.invalid=无效或已过期的用户验证码 error.deviceAuth.deviceCode.expired=设备验证码已过期 error.deviceAuth.deviceCode.invalid=设备验证码无效或已过期 error.deviceAuth.deviceCode.used=设备验证码已被使用 +error.admin.user.notFound=鐢ㄦ埛涓嶅瓨鍦細{0} +error.admin.user.role.invalid=鏃犳晥鐨勮鑹诧細{0} +error.admin.user.role.superAdmin.assignDenied=鍙湁 SUPER_ADMIN 鍙互鍒嗛厤 SUPER_ADMIN 瑙掕壊 +error.admin.user.status.invalid=鏃犳晥鐨勭敤鎴风姸鎬侊細{0} +error.admin.user.status.unsupported=杩欓噷鍙厑璁告寜 ACTIVE 鎴?DISABLED 绠$悊鐢ㄦ埛鐘舵€? diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/compat/ClawHubCompatControllerTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/compat/ClawHubCompatControllerTest.java index 90e6b164..e9c72230 100644 --- a/server/skillhub-app/src/test/java/com/iflytek/skillhub/compat/ClawHubCompatControllerTest.java +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/compat/ClawHubCompatControllerTest.java @@ -3,6 +3,8 @@ package com.iflytek.skillhub.compat; import com.iflytek.skillhub.auth.rbac.PlatformPrincipal; import com.iflytek.skillhub.domain.namespace.NamespaceMemberRepository; import com.iflytek.skillhub.auth.device.DeviceAuthService; +import com.iflytek.skillhub.dto.SkillSummaryResponse; +import com.iflytek.skillhub.service.SkillSearchAppService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; @@ -15,7 +17,10 @@ import org.springframework.test.web.servlet.MockMvc; import java.util.List; import java.util.Set; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import static org.mockito.Mockito.when; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -35,13 +40,38 @@ class ClawHubCompatControllerTest { @MockBean private DeviceAuthService deviceAuthService; + @MockBean + private SkillSearchAppService skillSearchAppService; + @Test - void search_returns_200() throws Exception { + void search_returns_mapped_results() throws Exception { + when(skillSearchAppService.search("test", null, "relevance", 0, 20, null, null)) + .thenReturn(new SkillSearchAppService.SearchResponse( + List.of(new SkillSummaryResponse( + 1L, + "my-skill", + "My Skill", + "test summary", + 10L, + 5, + BigDecimal.valueOf(4.5), + 2, + "1.2.0", + "global", + LocalDateTime.of(2026, 3, 13, 9, 0))), + 1, + 0, + 20 + )); + mockMvc.perform(get("/api/compat/v1/search") .param("q", "test")) .andExpect(status().isOk()) .andExpect(jsonPath("$.items").isArray()) - .andExpect(jsonPath("$.items").isEmpty()); + .andExpect(jsonPath("$.items[0].canonicalSlug").value("my-skill")) + .andExpect(jsonPath("$.items[0].description").value("test summary")) + .andExpect(jsonPath("$.items[0].latestVersion").value("1.2.0")) + .andExpect(jsonPath("$.items[0].starCount").value(5)); } @Test diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/admin/AuditLogControllerTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/admin/AuditLogControllerTest.java index becb9cdd..d255616d 100644 --- a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/admin/AuditLogControllerTest.java +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/admin/AuditLogControllerTest.java @@ -4,6 +4,9 @@ import com.iflytek.skillhub.TestRedisConfig; import com.iflytek.skillhub.auth.rbac.PlatformPrincipal; import com.iflytek.skillhub.auth.device.DeviceAuthService; import com.iflytek.skillhub.domain.namespace.NamespaceMemberRepository; +import com.iflytek.skillhub.dto.AuditLogItemResponse; +import com.iflytek.skillhub.dto.PageResponse; +import com.iflytek.skillhub.service.AdminAuditLogAppService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; @@ -17,7 +20,9 @@ import org.springframework.test.web.servlet.MockMvc; import java.util.List; import java.util.Set; +import java.time.Instant; +import static org.mockito.Mockito.when; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -38,6 +43,9 @@ class AuditLogControllerTest { @MockBean private DeviceAuthService deviceAuthService; + @MockBean + private AdminAuditLogAppService adminAuditLogAppService; + @Test void listAuditLogs_unauthenticated_returns401() throws Exception { mockMvc.perform(get("/api/v1/admin/audit-logs")) @@ -53,11 +61,27 @@ class AuditLogControllerTest { principal, null, List.of(new SimpleGrantedAuthority("ROLE_AUDITOR")) ); + when(adminAuditLogAppService.listAuditLogs(0, 20, null, null)) + .thenReturn(new PageResponse<>( + List.of(new AuditLogItemResponse( + 1L, + "USER_STATUS_CHANGE", + "user-1", + "alice", + "{\"status\":\"DISABLED\"}", + "127.0.0.1", + Instant.parse("2026-03-13T01:00:00Z"))), + 1, + 0, + 20)); + mockMvc.perform(get("/api/v1/admin/audit-logs").with(authentication(auth))) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(0)) .andExpect(jsonPath("$.data.items").isArray()) - .andExpect(jsonPath("$.data.total").value(2)); + .andExpect(jsonPath("$.data.total").value(1)) + .andExpect(jsonPath("$.data.items[0].username").value("alice")) + .andExpect(jsonPath("$.data.items[0].details").value("{\"status\":\"DISABLED\"}")); } @Test @@ -69,6 +93,9 @@ class AuditLogControllerTest { principal, null, List.of(new SimpleGrantedAuthority("ROLE_SUPER_ADMIN")) ); + when(adminAuditLogAppService.listAuditLogs(0, 20, null, null)) + .thenReturn(new PageResponse<>(List.of(), 0, 0, 20)); + mockMvc.perform(get("/api/v1/admin/audit-logs").with(authentication(auth))) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.items").isArray()); @@ -83,6 +110,9 @@ class AuditLogControllerTest { principal, null, List.of(new SimpleGrantedAuthority("ROLE_AUDITOR")) ); + when(adminAuditLogAppService.listAuditLogs(0, 20, "user-1", "CREATE_SKILL")) + .thenReturn(new PageResponse<>(List.of(), 0, 0, 20)); + mockMvc.perform(get("/api/v1/admin/audit-logs") .param("userId", "user-1") .param("action", "CREATE_SKILL") diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/admin/UserManagementControllerTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/admin/UserManagementControllerTest.java index f642cb7c..8379a594 100644 --- a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/admin/UserManagementControllerTest.java +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/admin/UserManagementControllerTest.java @@ -4,6 +4,10 @@ import com.iflytek.skillhub.TestRedisConfig; import com.iflytek.skillhub.auth.rbac.PlatformPrincipal; import com.iflytek.skillhub.auth.device.DeviceAuthService; import com.iflytek.skillhub.domain.namespace.NamespaceMemberRepository; +import com.iflytek.skillhub.dto.AdminUserMutationResponse; +import com.iflytek.skillhub.dto.AdminUserSummaryResponse; +import com.iflytek.skillhub.dto.PageResponse; +import com.iflytek.skillhub.service.AdminUserAppService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; @@ -17,6 +21,7 @@ import org.springframework.test.web.servlet.MockMvc; import java.util.List; import java.util.Set; +import java.time.LocalDateTime; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; @@ -25,6 +30,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.mockito.Mockito.when; @SpringBootTest @AutoConfigureMockMvc @@ -41,6 +47,9 @@ class UserManagementControllerTest { @MockBean private DeviceAuthService deviceAuthService; + @MockBean + private AdminUserAppService adminUserAppService; + @Test void listUsers_unauthenticated_returns401() throws Exception { mockMvc.perform(get("/api/v1/admin/users")) @@ -56,11 +65,27 @@ class UserManagementControllerTest { principal, null, List.of(new SimpleGrantedAuthority("ROLE_USER_ADMIN")) ); + when(adminUserAppService.listUsers(null, null, 0, 20)) + .thenReturn(new PageResponse<>( + List.of(new AdminUserSummaryResponse( + "user-1", + "alice", + "alice@example.com", + "ACTIVE", + List.of("AUDITOR"), + LocalDateTime.of(2026, 3, 13, 9, 0))), + 1, + 0, + 20)); + mockMvc.perform(get("/api/v1/admin/users").with(authentication(auth))) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(0)) .andExpect(jsonPath("$.data.items").isArray()) - .andExpect(jsonPath("$.data.total").value(2)); + .andExpect(jsonPath("$.data.total").value(1)) + .andExpect(jsonPath("$.data.items[0].id").value("user-1")) + .andExpect(jsonPath("$.data.items[0].email").value("alice@example.com")) + .andExpect(jsonPath("$.data.items[0].platformRoles[0]").value("AUDITOR")); } @Test @@ -72,6 +97,9 @@ class UserManagementControllerTest { principal, null, List.of(new SimpleGrantedAuthority("ROLE_SUPER_ADMIN")) ); + when(adminUserAppService.listUsers(null, null, 0, 20)) + .thenReturn(new PageResponse<>(List.of(), 0, 0, 20)); + mockMvc.perform(get("/api/v1/admin/users").with(authentication(auth))) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.items").isArray()); @@ -88,6 +116,9 @@ class UserManagementControllerTest { String requestBody = "{\"role\":\"MODERATOR\"}"; + when(adminUserAppService.updateUserRole("user-123", "MODERATOR", Set.of("USER_ADMIN"))) + .thenReturn(new AdminUserMutationResponse("user-123", "MODERATOR", "ACTIVE")); + mockMvc.perform(put("/api/v1/admin/users/user-123/role") .with(authentication(auth)) .with(csrf()) @@ -96,7 +127,8 @@ class UserManagementControllerTest { .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(0)) .andExpect(jsonPath("$.data.userId").value("user-123")) - .andExpect(jsonPath("$.data.role").value("MODERATOR")); + .andExpect(jsonPath("$.data.role").value("MODERATOR")) + .andExpect(jsonPath("$.data.status").value("ACTIVE")); } @Test @@ -108,7 +140,10 @@ class UserManagementControllerTest { principal, null, List.of(new SimpleGrantedAuthority("ROLE_USER_ADMIN")) ); - String requestBody = "{\"status\":\"BANNED\"}"; + String requestBody = "{\"status\":\"DISABLED\"}"; + + when(adminUserAppService.updateUserStatus("user-123", "DISABLED")) + .thenReturn(new AdminUserMutationResponse("user-123", null, "DISABLED")); mockMvc.perform(put("/api/v1/admin/users/user-123/status") .with(authentication(auth)) @@ -118,6 +153,6 @@ class UserManagementControllerTest { .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(0)) .andExpect(jsonPath("$.data.userId").value("user-123")) - .andExpect(jsonPath("$.data.status").value("BANNED")); + .andExpect(jsonPath("$.data.status").value("DISABLED")); } } diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/AdminAuditLogAppServiceTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/AdminAuditLogAppServiceTest.java new file mode 100644 index 00000000..5f2d64cc --- /dev/null +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/AdminAuditLogAppServiceTest.java @@ -0,0 +1,44 @@ +package com.iflytek.skillhub.service; + +import com.iflytek.skillhub.dto.AuditLogItemResponse; +import com.iflytek.skillhub.dto.PageResponse; +import org.junit.jupiter.api.Test; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; + +import java.time.Instant; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +class AdminAuditLogAppServiceTest { + + private final NamedParameterJdbcTemplate jdbcTemplate = mock(NamedParameterJdbcTemplate.class); + private final AdminAuditLogAppService service = new AdminAuditLogAppService(jdbcTemplate); + + @Test + void listAuditLogs_returnsJdbcBackedPage() { + when(jdbcTemplate.queryForObject(contains("COUNT(*)"), any(MapSqlParameterSource.class), eq(Long.class))) + .thenReturn(1L); + when(jdbcTemplate.query(contains("FROM audit_log"), any(MapSqlParameterSource.class), any(RowMapper.class))) + .thenReturn(List.of(new AuditLogItemResponse( + 1L, + "USER_STATUS_CHANGE", + "user-1", + "alice", + "{\"status\":\"DISABLED\"}", + "127.0.0.1", + Instant.parse("2026-03-13T01:00:00Z") + ))); + + PageResponse response = service.listAuditLogs(0, 20, "user-1", "USER_STATUS_CHANGE"); + + assertThat(response.total()).isEqualTo(1); + assertThat(response.items()).hasSize(1); + verify(jdbcTemplate).queryForObject(contains("al.actor_user_id = :userId"), any(MapSqlParameterSource.class), eq(Long.class)); + verify(jdbcTemplate).query(contains("al.action = :action"), any(MapSqlParameterSource.class), any(RowMapper.class)); + } +} diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/AdminUserAppServiceTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/AdminUserAppServiceTest.java new file mode 100644 index 00000000..03b7b853 --- /dev/null +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/AdminUserAppServiceTest.java @@ -0,0 +1,147 @@ +package com.iflytek.skillhub.service; + +import com.iflytek.skillhub.auth.entity.Role; +import com.iflytek.skillhub.auth.entity.UserRoleBinding; +import com.iflytek.skillhub.auth.repository.RoleRepository; +import com.iflytek.skillhub.auth.repository.UserRoleBindingRepository; +import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException; +import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException; +import com.iflytek.skillhub.domain.shared.exception.DomainNotFoundException; +import com.iflytek.skillhub.domain.user.UserAccount; +import com.iflytek.skillhub.domain.user.UserAccountRepository; +import com.iflytek.skillhub.domain.user.UserStatus; +import com.iflytek.skillhub.dto.PageResponse; +import com.iflytek.skillhub.repository.AdminUserSearchRepository; +import org.junit.jupiter.api.Test; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.test.util.ReflectionTestUtils; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +class AdminUserAppServiceTest { + + private final AdminUserSearchRepository adminUserSearchRepository = mock(AdminUserSearchRepository.class); + private final UserRoleBindingRepository userRoleBindingRepository = mock(UserRoleBindingRepository.class); + private final RoleRepository roleRepository = mock(RoleRepository.class); + private final UserAccountRepository userAccountRepository = mock(UserAccountRepository.class); + private final AdminUserAppService service = new AdminUserAppService( + adminUserSearchRepository, + userAccountRepository, + userRoleBindingRepository, + roleRepository + ); + + @Test + void listUsers_returnsPagedUsersFromRepository() { + UserAccount user = user("user-1", "alice", "alice@example.com", UserStatus.ACTIVE); + PageRequest pageable = PageRequest.of(0, 20, Sort.by(Sort.Direction.DESC, "createdAt")); + when(adminUserSearchRepository.search("ali", UserStatus.ACTIVE, pageable)) + .thenReturn(new PageImpl<>(List.of(user), pageable, 1)); + when(userRoleBindingRepository.findByUserIdIn(List.of("user-1"))) + .thenReturn(List.of(new UserRoleBinding("user-1", role("AUDITOR")))); + + PageResponse response = service.listUsers("ali", "ACTIVE", 0, 20); + + assertThat(response.total()).isEqualTo(1); + assertThat(response.items()).hasSize(1); + assertThat(response.items().get(0)).extracting("id", "username", "email", "status") + .containsExactly("user-1", "alice", "alice@example.com", "ACTIVE"); + assertThat(response.items().get(0)).extracting("platformRoles") + .isEqualTo(List.of("AUDITOR")); + } + + @Test + void listUsers_withInvalidStatus_throwsBadRequest() { + assertThrows(DomainBadRequestException.class, () -> service.listUsers(null, "BANNED", 0, 20)); + } + + @Test + void updateUserRole_nonSuperAdminCannotAssignSuperAdmin() { + when(userAccountRepository.findById("user-1")) + .thenReturn(Optional.of(user("user-1", "alice", "alice@example.com", UserStatus.ACTIVE))); + + assertThrows(DomainForbiddenException.class, + () -> service.updateUserRole("user-1", "SUPER_ADMIN", Set.of("USER_ADMIN"))); + } + + @Test + void updateUserRole_replacesExistingBindings() { + when(userAccountRepository.findById("user-1")) + .thenReturn(Optional.of(user("user-1", "alice", "alice@example.com", UserStatus.ACTIVE))); + when(roleRepository.findByCode("AUDITOR")).thenReturn(Optional.of(role("AUDITOR"))); + + var response = service.updateUserRole("user-1", "AUDITOR", Set.of("SUPER_ADMIN")); + + verify(userRoleBindingRepository).deleteByUserId("user-1"); + verify(userRoleBindingRepository).save(any(UserRoleBinding.class)); + assertThat(response.userId()).isEqualTo("user-1"); + assertThat(response.role()).isEqualTo("AUDITOR"); + assertThat(response.status()).isEqualTo("ACTIVE"); + } + + @Test + void updateUserRole_userPseudoRoleClearsBindingsWithoutSavingNewRole() { + when(userAccountRepository.findById("user-1")) + .thenReturn(Optional.of(user("user-1", "alice", "alice@example.com", UserStatus.ACTIVE))); + + var response = service.updateUserRole("user-1", "USER", Set.of("SUPER_ADMIN")); + + verify(userRoleBindingRepository).deleteByUserId("user-1"); + verify(userRoleBindingRepository, never()).save(any(UserRoleBinding.class)); + assertThat(response.role()).isEqualTo("USER"); + } + + @Test + void updateUserStatus_rejectsUnsupportedStatuses() { + when(userAccountRepository.findById("user-1")) + .thenReturn(Optional.of(user("user-1", "alice", "alice@example.com", UserStatus.ACTIVE))); + + assertThrows(DomainBadRequestException.class, () -> service.updateUserStatus("user-1", "MERGED")); + } + + @Test + void updateUserStatus_updatesPersistedStatus() { + UserAccount user = user("user-1", "alice", "alice@example.com", UserStatus.ACTIVE); + when(userAccountRepository.findById("user-1")).thenReturn(Optional.of(user)); + when(userAccountRepository.save(user)).thenReturn(user); + + var response = service.updateUserStatus("user-1", "DISABLED"); + + verify(userAccountRepository).save(user); + assertThat(user.getStatus()).isEqualTo(UserStatus.DISABLED); + assertThat(response.status()).isEqualTo("DISABLED"); + } + + @Test + void updateUserStatus_withUnknownUser_throwsNotFound() { + when(userAccountRepository.findById("missing")).thenReturn(Optional.empty()); + + assertThrows(DomainNotFoundException.class, () -> service.updateUserStatus("missing", "DISABLED")); + } + + private UserAccount user(String id, String displayName, String email, UserStatus status) { + UserAccount user = new UserAccount(id, displayName, email, null); + user.setStatus(status); + ReflectionTestUtils.setField(user, "createdAt", LocalDateTime.of(2026, 3, 13, 9, 0)); + ReflectionTestUtils.setField(user, "updatedAt", LocalDateTime.of(2026, 3, 13, 9, 0)); + return user; + } + + private Role role(String code) { + Role role = new Role(); + ReflectionTestUtils.setField(role, "code", code); + ReflectionTestUtils.setField(role, "name", code); + return role; + } +} diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/repository/UserRoleBindingRepository.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/repository/UserRoleBindingRepository.java index 76aeaf6b..def4ad3b 100644 --- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/repository/UserRoleBindingRepository.java +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/repository/UserRoleBindingRepository.java @@ -3,9 +3,13 @@ package com.iflytek.skillhub.auth.repository; import com.iflytek.skillhub.auth.entity.UserRoleBinding; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; + +import java.util.Collection; import java.util.List; @Repository public interface UserRoleBindingRepository extends JpaRepository { List findByUserId(String userId); + List findByUserIdIn(Collection userIds); + void deleteByUserId(String userId); } diff --git a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/UserAccountJpaRepository.java b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/UserAccountJpaRepository.java index 4f6e9eb7..c7dcc3ca 100644 --- a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/UserAccountJpaRepository.java +++ b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/UserAccountJpaRepository.java @@ -3,9 +3,10 @@ package com.iflytek.skillhub.infra.jpa; import com.iflytek.skillhub.domain.user.UserAccount; import com.iflytek.skillhub.domain.user.UserAccountRepository; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.stereotype.Repository; @Repository public interface UserAccountJpaRepository - extends JpaRepository, UserAccountRepository { + extends JpaRepository, JpaSpecificationExecutor, UserAccountRepository { }