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 ef8a4f31..5ab0e4bb 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 @@ -304,8 +304,7 @@ public class ClawHubCompatController { Namespace ns = namespaceRepository.findBySlug(coord.namespace()) .orElseThrow(() -> new DomainNotFoundException("error.namespace.notFound", coord.namespace())); - Skill skill = skillRepository.findByNamespaceIdAndSlug(ns.getId(), coord.slug()) - .orElseThrow(() -> new DomainNotFoundException("error.skill.notFound", coord.slug())); + Skill skill = resolveVisibleSkill(ns.getId(), coord.slug(), userId); SkillVersion latestVersionEntity = null; if (skill.getLatestVersionId() != null) { @@ -382,8 +381,7 @@ public class ClawHubCompatController { SkillCoordinate coord = mapper.fromCanonical(canonicalSlug); Namespace ns = namespaceRepository.findBySlug(coord.namespace()) .orElseThrow(() -> new DomainNotFoundException("error.namespace.notFound", coord.namespace())); - Skill skill = skillRepository.findByNamespaceIdAndSlug(ns.getId(), coord.slug()) - .orElseThrow(() -> new DomainNotFoundException("error.skill.notFound", canonicalSlug)); + Skill skill = resolveVisibleSkill(ns.getId(), coord.slug(), principal.userId()); boolean alreadyStarred = skillStarService.isStarred(skill.getId(), principal.userId()); skillStarService.star(skill.getId(), principal.userId()); @@ -399,8 +397,7 @@ public class ClawHubCompatController { SkillCoordinate coord = mapper.fromCanonical(canonicalSlug); Namespace ns = namespaceRepository.findBySlug(coord.namespace()) .orElseThrow(() -> new DomainNotFoundException("error.namespace.notFound", coord.namespace())); - Skill skill = skillRepository.findByNamespaceIdAndSlug(ns.getId(), coord.slug()) - .orElseThrow(() -> new DomainNotFoundException("error.skill.notFound", canonicalSlug)); + Skill skill = resolveVisibleSkill(ns.getId(), coord.slug(), principal.userId()); boolean alreadyUnstarred = !skillStarService.isStarred(skill.getId(), principal.userId()); skillStarService.unstar(skill.getId(), principal.userId()); @@ -482,4 +479,26 @@ public class ClawHubCompatController { principal.avatarUrl() ); } + + private Skill resolveVisibleSkill(Long namespaceId, String slug, String currentUserId) { + java.util.List skills = skillRepository.findByNamespaceIdAndSlug(namespaceId, slug); + if (skills.isEmpty()) { + throw new DomainNotFoundException("error.skill.notFound", slug); + } + java.util.Optional published = skills.stream() + .filter(s -> s.getLatestVersionId() != null) + .findFirst(); + if (published.isPresent()) { + return published.get(); + } + if (currentUserId != null) { + java.util.Optional ownSkill = skills.stream() + .filter(s -> currentUserId.equals(s.getOwnerId())) + .findFirst(); + if (ownSkill.isPresent()) { + return ownSkill.get(); + } + } + return skills.get(0); + } } diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillLifecycleController.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillLifecycleController.java index 7bb2964c..42b357cc 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillLifecycleController.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillLifecycleController.java @@ -66,7 +66,7 @@ public class SkillLifecycleController extends BaseApiController { @RequestAttribute("userId") String userId, @RequestAttribute(value = "userNsRoles", required = false) Map userNsRoles, HttpServletRequest httpRequest) { - Skill skill = findSkill(namespace, slug); + Skill skill = findSkill(namespace, slug, userId); Skill archived = skillGovernanceService.archiveSkill( skill.getId(), userId, @@ -86,7 +86,7 @@ public class SkillLifecycleController extends BaseApiController { @RequestAttribute("userId") String userId, @RequestAttribute(value = "userNsRoles", required = false) Map userNsRoles, HttpServletRequest httpRequest) { - Skill skill = findSkill(namespace, slug); + Skill skill = findSkill(namespace, slug, userId); Skill restored = skillGovernanceService.unarchiveSkill( skill.getId(), userId, @@ -106,7 +106,7 @@ public class SkillLifecycleController extends BaseApiController { @RequestAttribute("userId") String userId, @RequestAttribute(value = "userNsRoles", required = false) Map userNsRoles, HttpServletRequest httpRequest) { - Skill skill = findSkill(namespace, slug); + Skill skill = findSkill(namespace, slug, userId); SkillVersion skillVersion = skillVersionRepository.findBySkillIdAndVersion(skill.getId(), version) .orElseThrow(() -> new DomainBadRequestException("error.skill.version.notFound", version)); skillGovernanceService.deleteVersion( @@ -128,7 +128,7 @@ public class SkillLifecycleController extends BaseApiController { @PathVariable String version, @RequestAttribute("userId") String userId, HttpServletRequest httpRequest) { - Skill skill = findSkill(namespace, slug); + Skill skill = findSkill(namespace, slug, userId); SkillVersion skillVersion = skillVersionRepository.findBySkillIdAndVersion(skill.getId(), version) .orElseThrow(() -> new DomainBadRequestException("error.skill.version.notFound", version)); reviewService.withdrawReview(skillVersion.getId(), userId); @@ -155,7 +155,7 @@ public class SkillLifecycleController extends BaseApiController { @RequestAttribute("userId") String userId, @RequestAttribute(value = "userNsRoles", required = false) Map userNsRoles, HttpServletRequest httpRequest) { - Skill skill = findSkill(namespace, slug); + Skill skill = findSkill(namespace, slug, userId); SkillVersion skillVersion = skillVersionRepository.findBySkillIdAndVersion(skill.getId(), version) .orElseThrow(() -> new DomainBadRequestException("error.skill.version.notFound", version)); SkillPublishService.PublishResult result = skillPublishService.rereleasePublishedVersion( @@ -181,11 +181,32 @@ public class SkillLifecycleController extends BaseApiController { new SkillLifecycleMutationResponse(result.skillId(), result.version().getId(), "RERELEASE_VERSION", result.version().getStatus().name())); } - private Skill findSkill(String namespaceSlug, String skillSlug) { + private Skill findSkill(String namespaceSlug, String skillSlug, String currentUserId) { String cleanNamespace = namespaceSlug.startsWith("@") ? namespaceSlug.substring(1) : namespaceSlug; Namespace namespace = namespaceRepository.findBySlug(cleanNamespace) .orElseThrow(() -> new DomainBadRequestException("error.namespace.slug.notFound", cleanNamespace)); - return skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug) - .orElseThrow(() -> new DomainBadRequestException("error.skill.notFound", skillSlug)); + return resolveVisibleSkill(namespace.getId(), skillSlug, currentUserId); + } + + private Skill resolveVisibleSkill(Long namespaceId, String slug, String currentUserId) { + java.util.List skills = skillRepository.findByNamespaceIdAndSlug(namespaceId, slug); + if (skills.isEmpty()) { + throw new DomainBadRequestException("error.skill.notFound", slug); + } + java.util.Optional published = skills.stream() + .filter(s -> s.getLatestVersionId() != null) + .findFirst(); + if (published.isPresent()) { + return published.get(); + } + if (currentUserId != null) { + java.util.Optional ownSkill = skills.stream() + .filter(s -> currentUserId.equals(s.getOwnerId())) + .findFirst(); + if (ownSkill.isPresent()) { + return ownSkill.get(); + } + } + return skills.get(0); } } diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillReportController.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillReportController.java index ca9c5b3a..a8daa230 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillReportController.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillReportController.java @@ -43,7 +43,7 @@ public class SkillReportController extends BaseApiController { @RequestBody SkillReportSubmitRequest request, @RequestAttribute("userId") String userId, HttpServletRequest httpRequest) { - Skill skill = findSkill(namespace, slug); + Skill skill = findSkill(namespace, slug, userId); var report = skillReportService.submitReport( skill.getId(), userId, @@ -55,11 +55,32 @@ public class SkillReportController extends BaseApiController { return ok("response.success.created", new SkillReportMutationResponse(report.getId(), report.getStatus().name())); } - private Skill findSkill(String namespaceSlug, String skillSlug) { + private Skill findSkill(String namespaceSlug, String skillSlug, String currentUserId) { String cleanNamespace = namespaceSlug.startsWith("@") ? namespaceSlug.substring(1) : namespaceSlug; Namespace namespace = namespaceRepository.findBySlug(cleanNamespace) .orElseThrow(() -> new DomainBadRequestException("error.namespace.slug.notFound", cleanNamespace)); - return skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug) - .orElseThrow(() -> new DomainBadRequestException("error.skill.notFound", skillSlug)); + return resolveVisibleSkill(namespace.getId(), skillSlug, currentUserId); } -} + + private Skill resolveVisibleSkill(Long namespaceId, String slug, String currentUserId) { + java.util.List skills = skillRepository.findByNamespaceIdAndSlug(namespaceId, slug); + if (skills.isEmpty()) { + throw new DomainBadRequestException("error.skill.notFound", slug); + } + java.util.Optional published = skills.stream() + .filter(s -> s.getLatestVersionId() != null) + .findFirst(); + if (published.isPresent()) { + return published.get(); + } + if (currentUserId != null) { + java.util.Optional ownSkill = skills.stream() + .filter(s -> currentUserId.equals(s.getOwnerId())) + .findFirst(); + if (ownSkill.isPresent()) { + return ownSkill.get(); + } + } + return skills.get(0); + } +} \ No newline at end of file diff --git a/server/skillhub-app/src/main/resources/db/migration/V12__skill_owner_uniqueness.sql b/server/skillhub-app/src/main/resources/db/migration/V12__skill_owner_uniqueness.sql new file mode 100644 index 00000000..f1fac58b --- /dev/null +++ b/server/skillhub-app/src/main/resources/db/migration/V12__skill_owner_uniqueness.sql @@ -0,0 +1,6 @@ +-- V12__skill_owner_uniqueness.sql +-- Change skill uniqueness from (namespace_id, slug) to (namespace_id, slug, owner_id) +-- to support owner-isolated skill records with the same name + +ALTER TABLE skill DROP CONSTRAINT skill_namespace_id_slug_key; +ALTER TABLE skill ADD CONSTRAINT skill_namespace_id_slug_owner_id_key UNIQUE(namespace_id, slug, owner_id); diff --git a/server/skillhub-app/src/main/resources/messages.properties b/server/skillhub-app/src/main/resources/messages.properties index d1728926..e92a2ae8 100644 --- a/server/skillhub-app/src/main/resources/messages.properties +++ b/server/skillhub-app/src/main/resources/messages.properties @@ -119,3 +119,5 @@ 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 +error.skill.publish.nameConflict=A published skill with name ''{0}'' already exists in this namespace +error.skill.approve.nameConflict=Cannot approve: a published skill with name ''{0}'' already exists in this namespace diff --git a/server/skillhub-app/src/main/resources/messages_zh.properties b/server/skillhub-app/src/main/resources/messages_zh.properties index ba3ad200..d66420e1 100644 --- a/server/skillhub-app/src/main/resources/messages_zh.properties +++ b/server/skillhub-app/src/main/resources/messages_zh.properties @@ -119,3 +119,5 @@ 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 状态的用户 +error.skill.publish.nameConflict=该命名空间下已存在名为"{0}"的已发布技能,无法提交 +error.skill.approve.nameConflict=无法通过审核:该命名空间下已存在名为"{0}"的已发布技能 diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/portal/SkillLifecycleControllerTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/portal/SkillLifecycleControllerTest.java index 26613505..6705325d 100644 --- a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/portal/SkillLifecycleControllerTest.java +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/portal/SkillLifecycleControllerTest.java @@ -81,7 +81,7 @@ class SkillLifecycleControllerTest { setSkillId(skill, 1L); given(namespaceRepository.findBySlug("global")).willReturn(java.util.Optional.of(namespace)); - given(skillRepository.findByNamespaceIdAndSlug(1L, "demo-skill")).willReturn(java.util.Optional.of(skill)); + given(skillRepository.findByNamespaceIdAndSlug(1L, "demo-skill")).willReturn(java.util.List.of(skill)); given(skillGovernanceService.archiveSkill(eq(1L), eq("usr_1"), anyMap(), nullable(String.class), nullable(String.class), eq("cleanup"))) .willReturn(skillWithStatus(skill, com.iflytek.skillhub.domain.skill.SkillStatus.ARCHIVED)); @@ -108,7 +108,7 @@ class SkillLifecycleControllerTest { skill.setStatus(com.iflytek.skillhub.domain.skill.SkillStatus.ARCHIVED); given(namespaceRepository.findBySlug("global")).willReturn(java.util.Optional.of(namespace)); - given(skillRepository.findByNamespaceIdAndSlug(1L, "demo-skill")).willReturn(java.util.Optional.of(skill)); + given(skillRepository.findByNamespaceIdAndSlug(1L, "demo-skill")).willReturn(java.util.List.of(skill)); given(skillGovernanceService.unarchiveSkill(eq(1L), eq("usr_1"), anyMap(), nullable(String.class), nullable(String.class))) .willReturn(skillWithStatus(skill, com.iflytek.skillhub.domain.skill.SkillStatus.ACTIVE)); @@ -135,7 +135,7 @@ class SkillLifecycleControllerTest { version.setStatus(SkillVersionStatus.DRAFT); given(namespaceRepository.findBySlug("global")).willReturn(java.util.Optional.of(namespace)); - given(skillRepository.findByNamespaceIdAndSlug(1L, "demo-skill")).willReturn(java.util.Optional.of(skill)); + given(skillRepository.findByNamespaceIdAndSlug(1L, "demo-skill")).willReturn(java.util.List.of(skill)); given(skillVersionRepository.findBySkillIdAndVersion(1L, "1.0.0")).willReturn(java.util.Optional.of(version)); mockMvc.perform(delete("/api/web/skills/global/demo-skill/versions/1.0.0") @@ -162,7 +162,7 @@ class SkillLifecycleControllerTest { version.setStatus(SkillVersionStatus.PENDING_REVIEW); given(namespaceRepository.findBySlug("global")).willReturn(java.util.Optional.of(namespace)); - given(skillRepository.findByNamespaceIdAndSlug(1L, "demo-skill")).willReturn(java.util.Optional.of(skill)); + given(skillRepository.findByNamespaceIdAndSlug(1L, "demo-skill")).willReturn(java.util.List.of(skill)); given(skillVersionRepository.findBySkillIdAndVersion(1L, "1.0.0")).willReturn(java.util.Optional.of(version)); mockMvc.perform(post("/api/web/skills/global/demo-skill/versions/1.0.0/withdraw-review") @@ -188,7 +188,7 @@ class SkillLifecycleControllerTest { newVersion.setStatus(SkillVersionStatus.PUBLISHED); given(namespaceRepository.findBySlug("global")).willReturn(java.util.Optional.of(namespace)); - given(skillRepository.findByNamespaceIdAndSlug(1L, "demo-skill")).willReturn(java.util.Optional.of(skill)); + given(skillRepository.findByNamespaceIdAndSlug(1L, "demo-skill")).willReturn(java.util.List.of(skill)); SkillVersion sourceVersion = new SkillVersion(1L, "1.2.3", "owner"); setSkillVersionId(sourceVersion, 2L); sourceVersion.setStatus(SkillVersionStatus.PUBLISHED); diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/portal/SkillReportControllerTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/portal/SkillReportControllerTest.java index 4f886b7a..29fab816 100644 --- a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/portal/SkillReportControllerTest.java +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/portal/SkillReportControllerTest.java @@ -64,7 +64,7 @@ class SkillReportControllerTest { ReflectionTestUtils.setField(report, "id", 99L); given(namespaceRepository.findBySlug("global")).willReturn(java.util.Optional.of(namespace)); - given(skillRepository.findByNamespaceIdAndSlug(1L, "demo-skill")).willReturn(java.util.Optional.of(skill)); + given(skillRepository.findByNamespaceIdAndSlug(1L, "demo-skill")).willReturn(java.util.List.of(skill)); given(skillReportService.submitReport(eq(10L), eq("user-1"), eq("Spam"), eq("details"), nullable(String.class), nullable(String.class))) .willReturn(report); diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewService.java index ec99bbc6..f7e047b9 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewService.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewService.java @@ -24,6 +24,7 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.ConcurrentModificationException; +import java.util.List; import java.util.Map; import java.util.Set; @@ -153,12 +154,27 @@ public class ReviewService { SkillVersion skillVersion = skillVersionRepository.findById(task.getSkillVersionId()) .orElseThrow(() -> new DomainNotFoundException("skill_version.not_found", task.getSkillVersionId())); + + Skill skill = skillRepository.findById(skillVersion.getSkillId()) + .orElseThrow(() -> new DomainNotFoundException("skill.not_found", skillVersion.getSkillId())); + + // Check no other owner has a published skill with the same slug + List sameSlugSkills = skillRepository.findByNamespaceIdAndSlug(skill.getNamespaceId(), skill.getSlug()); + for (Skill other : sameSlugSkills) { + if (!other.getId().equals(skill.getId())) { + boolean otherHasPublished = !skillVersionRepository + .findBySkillIdAndStatus(other.getId(), SkillVersionStatus.PUBLISHED) + .isEmpty(); + if (otherHasPublished) { + throw new DomainBadRequestException("error.skill.approve.nameConflict", skill.getSlug()); + } + } + } + skillVersion.setStatus(SkillVersionStatus.PUBLISHED); skillVersion.setPublishedAt(LocalDateTime.now()); skillVersionRepository.save(skillVersion); - Skill skill = skillRepository.findById(skillVersion.getSkillId()) - .orElseThrow(() -> new DomainNotFoundException("skill.not_found", skillVersion.getSkillId())); skill.setLatestVersionId(skillVersion.getId()); applyPublishedMetadata(skill, skillVersion); skill.setUpdatedBy(reviewerId); diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/SkillRepository.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/SkillRepository.java index fa34494c..6bdf2f03 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/SkillRepository.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/SkillRepository.java @@ -7,7 +7,8 @@ public interface SkillRepository { Optional findById(Long id); List findByIdIn(List ids); List findAll(); - Optional findByNamespaceIdAndSlug(Long namespaceId, String slug); + List findByNamespaceIdAndSlug(Long namespaceId, String slug); + Optional findByNamespaceIdAndSlugAndOwnerId(Long namespaceId, String slug, String ownerId); List findByNamespaceIdAndStatus(Long namespaceId, SkillStatus status); Skill save(Skill skill); void delete(Skill skill); diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillDownloadService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillDownloadService.java index 8d18b875..a53f1bbc 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillDownloadService.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillDownloadService.java @@ -14,7 +14,9 @@ import org.springframework.stereotype.Service; import java.io.InputStream; import java.time.Duration; +import java.util.List; import java.util.Map; +import java.util.Optional; @Service public class SkillDownloadService { @@ -59,8 +61,7 @@ public class SkillDownloadService { Map userNsRoles) { Namespace namespace = findNamespace(namespaceSlug); - Skill skill = skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug) - .orElseThrow(() -> new DomainBadRequestException("error.skill.notFound", skillSlug)); + Skill skill = resolveVisibleSkill(namespace.getId(), skillSlug, currentUserId); // Visibility check if (!visibilityChecker.canAccess(skill, currentUserId, userNsRoles)) { @@ -85,8 +86,7 @@ public class SkillDownloadService { Map userNsRoles) { Namespace namespace = findNamespace(namespaceSlug); - Skill skill = skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug) - .orElseThrow(() -> new DomainBadRequestException("error.skill.notFound", skillSlug)); + Skill skill = resolveVisibleSkill(namespace.getId(), skillSlug, currentUserId); // Visibility check if (!visibilityChecker.canAccess(skill, currentUserId, userNsRoles)) { @@ -107,8 +107,7 @@ public class SkillDownloadService { Map userNsRoles) { Namespace namespace = findNamespace(namespaceSlug); - Skill skill = skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug) - .orElseThrow(() -> new DomainBadRequestException("error.skill.notFound", skillSlug)); + Skill skill = resolveVisibleSkill(namespace.getId(), skillSlug, currentUserId); // Visibility check if (!visibilityChecker.canAccess(skill, currentUserId, userNsRoles)) { @@ -155,6 +154,28 @@ public class SkillDownloadService { .orElseThrow(() -> new DomainBadRequestException("error.namespace.slug.notFound", slug)); } + private Skill resolveVisibleSkill(Long namespaceId, String slug, String currentUserId) { + List skills = skillRepository.findByNamespaceIdAndSlug(namespaceId, slug); + if (skills.isEmpty()) { + throw new DomainBadRequestException("error.skill.notFound", slug); + } + Optional published = skills.stream() + .filter(s -> s.getLatestVersionId() != null) + .findFirst(); + if (published.isPresent()) { + return published.get(); + } + if (currentUserId != null) { + Optional ownSkill = skills.stream() + .filter(s -> currentUserId.equals(s.getOwnerId())) + .findFirst(); + if (ownSkill.isPresent()) { + return ownSkill.get(); + } + } + return skills.get(0); + } + private void assertPublishedAccessible(Skill skill) { if (skill.getStatus() != SkillStatus.ACTIVE) { throw new DomainBadRequestException("error.skill.status.notActive"); diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillPublishService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillPublishService.java index 24df91c3..406bf6f1 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillPublishService.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillPublishService.java @@ -188,8 +188,23 @@ public class SkillPublishService { String.join(", ", prePublishValidation.errors())); } - // 6. Find or create Skill record - Skill skill = skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug) + // 6. Find or create Skill record (with owner isolation) + List existingSkills = skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug); + + // Check if any other owner's skill has published versions + for (Skill existing : existingSkills) { + if (!existing.getOwnerId().equals(publisherId)) { + boolean hasPublished = !skillVersionRepository + .findBySkillIdAndStatus(existing.getId(), SkillVersionStatus.PUBLISHED) + .isEmpty(); + if (hasPublished) { + throw new DomainBadRequestException("error.skill.publish.nameConflict", skillSlug); + } + } + } + + // Find or create skill for current user + Skill skill = skillRepository.findByNamespaceIdAndSlugAndOwnerId(namespace.getId(), skillSlug, publisherId) .orElseGet(() -> { Skill newSkill = new Skill(namespace.getId(), skillSlug, publisherId, visibility); newSkill.setCreatedBy(publisherId); @@ -200,6 +215,14 @@ public class SkillPublishService { throw new DomainBadRequestException("error.skill.publish.archived", skillSlug); } + // 6c. Auto-withdraw pending review versions + List pendingVersions = skillVersionRepository + .findBySkillIdAndStatus(skill.getId(), SkillVersionStatus.PENDING_REVIEW); + for (SkillVersion pending : pendingVersions) { + pending.setStatus(SkillVersionStatus.DRAFT); + skillVersionRepository.save(pending); + } + // 7. Check version doesn't already exist if (skillVersionRepository.findBySkillIdAndVersion(skill.getId(), metadata.version()).isPresent()) { throw new DomainBadRequestException("error.skill.version.exists", metadata.version()); diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillQueryService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillQueryService.java index 32e84fa6..4b192bf2 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillQueryService.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillQueryService.java @@ -114,8 +114,7 @@ public class SkillQueryService { Map userNsRoles) { Namespace namespace = findNamespace(namespaceSlug); - Skill skill = skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug) - .orElseThrow(() -> new DomainBadRequestException("error.skill.notFound", skillSlug)); + Skill skill = resolveVisibleSkill(namespace.getId(), skillSlug, currentUserId); // Visibility check if (!visibilityChecker.canAccess(skill, currentUserId, userNsRoles)) { @@ -182,7 +181,7 @@ public class SkillQueryService { String currentUserId, Map userNsRoles) { Namespace namespace = findNamespace(namespaceSlug); - Skill skill = findSkill(namespace, skillSlug); + Skill skill = resolveVisibleSkill(namespace.getId(), skillSlug, currentUserId); assertPublishedAccessible(namespace, skill, currentUserId, userNsRoles); SkillVersion skillVersion = findVersion(skill, version); assertPreviewAccessible(skill, skillVersion, version, currentUserId); @@ -207,7 +206,7 @@ public class SkillQueryService { String currentUserId, Map userNsRoles) { Namespace namespace = findNamespace(namespaceSlug); - Skill skill = findSkill(namespace, skillSlug); + Skill skill = resolveVisibleSkill(namespace.getId(), skillSlug, currentUserId); assertPublishedAccessible(namespace, skill, currentUserId, userNsRoles); SkillVersion skillVersion = findVersion(skill, version); @@ -223,7 +222,7 @@ public class SkillQueryService { String currentUserId, Map userNsRoles) { Namespace namespace = findNamespace(namespaceSlug); - Skill skill = findSkill(namespace, skillSlug); + Skill skill = resolveVisibleSkill(namespace.getId(), skillSlug, currentUserId); assertPublishedAccessible(namespace, skill, currentUserId, userNsRoles); SkillVersion skillVersion = resolveVersionEntity(skill, null, tagName, null); return skillFileRepository.findByVersionId(skillVersion.getId()); @@ -237,7 +236,7 @@ public class SkillQueryService { String currentUserId, Map userNsRoles) { Namespace namespace = findNamespace(namespaceSlug); - Skill skill = findSkill(namespace, skillSlug); + Skill skill = resolveVisibleSkill(namespace.getId(), skillSlug, currentUserId); assertPublishedAccessible(namespace, skill, currentUserId, userNsRoles); SkillVersion skillVersion = findVersion(skill, version); @@ -256,7 +255,7 @@ public class SkillQueryService { String currentUserId, Map userNsRoles) { Namespace namespace = findNamespace(namespaceSlug); - Skill skill = findSkill(namespace, skillSlug); + Skill skill = resolveVisibleSkill(namespace.getId(), skillSlug, currentUserId); assertPublishedAccessible(namespace, skill, currentUserId, userNsRoles); SkillVersion skillVersion = resolveVersionEntity(skill, null, tagName, null); SkillFile file = findFile(skillVersion, filePath); @@ -269,7 +268,7 @@ public class SkillQueryService { Map userNsRoles, Pageable pageable) { Namespace namespace = findNamespace(namespaceSlug); - Skill skill = findSkill(namespace, skillSlug); + Skill skill = resolveVisibleSkill(namespace.getId(), skillSlug, currentUserId); assertPublishedAccessible(namespace, skill, currentUserId, userNsRoles); List visibleVersions; if (canManageRestrictedSkill(skill, currentUserId, userNsRoles)) { @@ -313,7 +312,7 @@ public class SkillQueryService { } Namespace namespace = findNamespace(namespaceSlug); - Skill skill = findSkill(namespace, skillSlug); + Skill skill = resolveVisibleSkill(namespace.getId(), skillSlug, currentUserId); assertPublishedAccessible(namespace, skill, currentUserId, userNsRoles); SkillVersion resolved = resolveVersionEntity(skill, version, tag, hash); String fingerprint = computeFingerprint(resolved); @@ -340,14 +339,30 @@ public class SkillQueryService { .orElseThrow(() -> new DomainBadRequestException("error.namespace.slug.notFound", slug)); } - private Skill findSkill(String namespaceSlug, String skillSlug) { + private Skill findSkill(String namespaceSlug, String skillSlug, String currentUserId) { Namespace namespace = findNamespace(namespaceSlug); - return findSkill(namespace, skillSlug); + return resolveVisibleSkill(namespace.getId(), skillSlug, currentUserId); } - private Skill findSkill(Namespace namespace, String skillSlug) { - return skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug) - .orElseThrow(() -> new DomainBadRequestException("error.skill.notFound", skillSlug)); + private Skill resolveVisibleSkill(Long namespaceId, String slug, String currentUserId) { + List skills = skillRepository.findByNamespaceIdAndSlug(namespaceId, slug); + if (skills.isEmpty()) { + throw new DomainBadRequestException("error.skill.notFound", slug); + } + + if (currentUserId != null) { + Optional ownSkill = skills.stream() + .filter(s -> currentUserId.equals(s.getOwnerId())) + .findFirst(); + if (ownSkill.isPresent()) { + return ownSkill.get(); + } + } + + return skills.stream() + .filter(s -> s.getLatestVersionId() != null) + .findFirst() + .orElseThrow(() -> new DomainBadRequestException("error.skill.notFound", slug)); } private SkillVersion findVersion(Skill skill, String version) { diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillTagService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillTagService.java index a6589562..b99ac0e2 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillTagService.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillTagService.java @@ -11,6 +11,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.Optional; @Service public class SkillTagService { @@ -44,8 +45,7 @@ public class SkillTagService { String currentUserId, java.util.Map userNamespaceRoles) { Namespace namespace = findNamespace(namespaceSlug); - Skill skill = skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug) - .orElseThrow(() -> new DomainBadRequestException("error.skill.notFound", skillSlug)); + Skill skill = resolveVisibleSkill(namespace.getId(), skillSlug, currentUserId); if (!visibilityChecker.canAccess(skill, currentUserId, userNamespaceRoles)) { throw new DomainForbiddenException("error.skill.access.denied", skillSlug); } @@ -76,8 +76,7 @@ public class SkillTagService { Namespace namespace = findNamespace(namespaceSlug); assertAdminOrOwner(namespace.getId(), operatorId); - Skill skill = skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug) - .orElseThrow(() -> new DomainBadRequestException("error.skill.notFound", skillSlug)); + Skill skill = resolveVisibleSkill(namespace.getId(), skillSlug, operatorId); // Find target version SkillVersion version = skillVersionRepository.findBySkillIdAndVersion(skill.getId(), targetVersion) @@ -111,8 +110,7 @@ public class SkillTagService { Namespace namespace = findNamespace(namespaceSlug); assertAdminOrOwner(namespace.getId(), operatorId); - Skill skill = skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug) - .orElseThrow(() -> new DomainBadRequestException("error.skill.notFound", skillSlug)); + Skill skill = resolveVisibleSkill(namespace.getId(), skillSlug, operatorId); SkillTag tag = skillTagRepository.findBySkillIdAndTagName(skill.getId(), tagName) .orElseThrow(() -> new DomainBadRequestException("error.skill.tag.notFound", tagName)); @@ -125,6 +123,28 @@ public class SkillTagService { .orElseThrow(() -> new DomainBadRequestException("error.namespace.slug.notFound", slug)); } + private Skill resolveVisibleSkill(Long namespaceId, String slug, String currentUserId) { + List skills = skillRepository.findByNamespaceIdAndSlug(namespaceId, slug); + if (skills.isEmpty()) { + throw new DomainBadRequestException("error.skill.notFound", slug); + } + Optional published = skills.stream() + .filter(s -> s.getLatestVersionId() != null) + .findFirst(); + if (published.isPresent()) { + return published.get(); + } + if (currentUserId != null) { + Optional ownSkill = skills.stream() + .filter(s -> currentUserId.equals(s.getOwnerId())) + .findFirst(); + if (ownSkill.isPresent()) { + return ownSkill.get(); + } + } + return skills.get(0); + } + private void assertAdminOrOwner(Long namespaceId, String operatorId) { NamespaceRole role = namespaceMemberRepository.findByNamespaceIdAndUserId(namespaceId, operatorId) .map(member -> member.getRole()) diff --git a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/review/ReviewServiceTest.java b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/review/ReviewServiceTest.java index 2ceb4562..b71cac4a 100644 --- a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/review/ReviewServiceTest.java +++ b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/review/ReviewServiceTest.java @@ -29,6 +29,7 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.dao.DataIntegrityViolationException; import java.util.ConcurrentModificationException; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -233,6 +234,7 @@ class ReviewServiceTest { .thenReturn(1); when(skillVersionRepository.findById(SKILL_VERSION_ID)).thenReturn(Optional.of(sv)); when(skillRepository.findById(SKILL_ID)).thenReturn(Optional.of(skill)); + when(skillRepository.findByNamespaceIdAndSlug(NAMESPACE_ID, "my-skill")).thenReturn(List.of(skill)); when(reviewTaskRepository.findById(REVIEW_TASK_ID)).thenReturn(Optional.of(task)); ReviewTask result = reviewService.approveReview( @@ -263,6 +265,7 @@ class ReviewServiceTest { when(reviewTaskRepository.updateStatusWithVersion(any(), any(), any(), any(), any())).thenReturn(1); when(skillVersionRepository.findById(SKILL_VERSION_ID)).thenReturn(Optional.of(sv)); when(skillRepository.findById(SKILL_ID)).thenReturn(Optional.of(skill)); + when(skillRepository.findByNamespaceIdAndSlug(NAMESPACE_ID, "my-skill")).thenReturn(List.of(skill)); when(reviewTaskRepository.findById(REVIEW_TASK_ID)).thenReturn(Optional.of(task)); reviewService.approveReview(REVIEW_TASK_ID, REVIEWER_ID, "ok", @@ -356,6 +359,7 @@ class ReviewServiceTest { .thenReturn(1); when(skillVersionRepository.findById(SKILL_VERSION_ID)).thenReturn(Optional.of(sv)); when(skillRepository.findById(SKILL_ID)).thenReturn(Optional.of(skill)); + when(skillRepository.findByNamespaceIdAndSlug(NAMESPACE_ID, "my-skill")).thenReturn(List.of(skill)); when(reviewTaskRepository.findById(REVIEW_TASK_ID)).thenReturn(Optional.of(task)); ReviewTask result = reviewService.approveReview( @@ -379,6 +383,33 @@ class ReviewServiceTest { () -> reviewService.approveReview(REVIEW_TASK_ID, REVIEWER_ID, "ok", Map.of(NAMESPACE_ID, NamespaceRole.ADMIN), Set.of())); } + + @Test + void shouldRejectApproveWhenOtherOwnerHasPublishedSameSlug() { + ReviewTask task = createPendingReviewTask(); + Namespace ns = createTeamNamespace(); + SkillVersion sv = createPendingReviewSkillVersion(); + Skill skill = createSkill(); // owned by USER_ID + + // Another owner's skill with same slug that has a published version + Skill otherSkill = new Skill(NAMESPACE_ID, "my-skill", "other-user", SkillVisibility.PUBLIC); + setField(otherSkill, "id", 99L); + SkillVersion otherPublished = new SkillVersion(99L, "1.0.0", "other-user"); + otherPublished.setStatus(SkillVersionStatus.PUBLISHED); + + when(reviewTaskRepository.findById(REVIEW_TASK_ID)).thenReturn(Optional.of(task)); + when(namespaceRepository.findById(NAMESPACE_ID)).thenReturn(Optional.of(ns)); + when(permissionChecker.canReview(any(), any(), any(), anyMap(), anySet())).thenReturn(true); + when(reviewTaskRepository.updateStatusWithVersion(any(), any(), any(), any(), any())).thenReturn(1); + when(skillVersionRepository.findById(SKILL_VERSION_ID)).thenReturn(Optional.of(sv)); + when(skillRepository.findById(SKILL_ID)).thenReturn(Optional.of(skill)); + when(skillRepository.findByNamespaceIdAndSlug(NAMESPACE_ID, "my-skill")).thenReturn(List.of(skill, otherSkill)); + when(skillVersionRepository.findBySkillIdAndStatus(99L, SkillVersionStatus.PUBLISHED)).thenReturn(List.of(otherPublished)); + + assertThrows(DomainBadRequestException.class, + () -> reviewService.approveReview(REVIEW_TASK_ID, REVIEWER_ID, "ok", + Map.of(NAMESPACE_ID, NamespaceRole.ADMIN), Set.of())); + } } @Nested diff --git a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillDownloadServiceTest.java b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillDownloadServiceTest.java index b077f24a..696c12fb 100644 --- a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillDownloadServiceTest.java +++ b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillDownloadServiceTest.java @@ -19,6 +19,7 @@ import java.io.ByteArrayInputStream; import java.io.InputStream; import java.lang.reflect.Field; import java.time.Instant; +import java.util.List; import java.util.Map; import java.util.Optional; @@ -82,7 +83,7 @@ class SkillDownloadServiceTest { ObjectMetadata metadata = new ObjectMetadata(1000L, "application/zip", Instant.now()); when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace)); - when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill)); + when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill)); when(visibilityChecker.canAccess(skill, userId, userNsRoles)).thenReturn(true); when(skillVersionRepository.findById(10L)).thenReturn(Optional.of(version)); when(objectStorageService.exists(storageKey)).thenReturn(true); @@ -124,7 +125,7 @@ class SkillDownloadServiceTest { ObjectMetadata metadata = new ObjectMetadata(1000L, "application/zip", Instant.now()); when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace)); - when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill)); + when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill)); when(visibilityChecker.canAccess(skill, userId, userNsRoles)).thenReturn(true); when(skillTagRepository.findBySkillIdAndTagName(1L, tagName)).thenReturn(Optional.of(tag)); when(skillVersionRepository.findById(10L)).thenReturn(Optional.of(version)); @@ -164,7 +165,7 @@ class SkillDownloadServiceTest { ObjectMetadata metadata = new ObjectMetadata(1000L, "application/zip", Instant.now()); when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace)); - when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill)); + when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill)); when(visibilityChecker.canAccess(skill, userId, userNsRoles)).thenReturn(true); when(skillVersionRepository.findBySkillIdAndVersion(1L, versionStr)).thenReturn(Optional.of(version)); when(objectStorageService.exists(storageKey)).thenReturn(true); @@ -196,7 +197,7 @@ class SkillDownloadServiceTest { version.setStatus(SkillVersionStatus.DRAFT); when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace)); - when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill)); + when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill)); when(visibilityChecker.canAccess(skill, userId, userNsRoles)).thenReturn(true); when(skillVersionRepository.findBySkillIdAndVersion(1L, versionStr)).thenReturn(Optional.of(version)); diff --git a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillPublishServiceTest.java b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillPublishServiceTest.java index 01c66828..27e87acc 100644 --- a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillPublishServiceTest.java +++ b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillPublishServiceTest.java @@ -109,7 +109,8 @@ class SkillPublishServiceTest { when(skillPackageValidator.validate(entries)).thenReturn(ValidationResult.pass()); when(skillMetadataParser.parse(skillMdContent)).thenReturn(metadata); when(prePublishValidator.validate(any())).thenReturn(ValidationResult.pass()); - when(skillRepository.findByNamespaceIdAndSlug(any(), eq("test-skill"))).thenReturn(Optional.of(skill)); + when(skillRepository.findByNamespaceIdAndSlug(any(), eq("test-skill"))).thenReturn(List.of(skill)); + when(skillRepository.findByNamespaceIdAndSlugAndOwnerId(any(), eq("test-skill"), eq(publisherId))).thenReturn(Optional.of(skill)); when(skillVersionRepository.findBySkillIdAndVersion(any(), eq("1.0.0"))).thenReturn(Optional.empty()); when(skillVersionRepository.save(any(SkillVersion.class))).thenAnswer(invocation -> { SkillVersion saved = invocation.getArgument(0); @@ -162,7 +163,8 @@ class SkillPublishServiceTest { when(skillPackageValidator.validate(entries)).thenReturn(ValidationResult.pass()); when(skillMetadataParser.parse(skillMdContent)).thenReturn(metadata); when(prePublishValidator.validate(any())).thenReturn(ValidationResult.pass()); - when(skillRepository.findByNamespaceIdAndSlug(any(), eq("smoke-skill-two"))).thenReturn(Optional.of(skill)); + when(skillRepository.findByNamespaceIdAndSlug(any(), eq("smoke-skill-two"))).thenReturn(List.of(skill)); + when(skillRepository.findByNamespaceIdAndSlugAndOwnerId(any(), eq("smoke-skill-two"), eq(publisherId))).thenReturn(Optional.of(skill)); when(skillVersionRepository.findBySkillIdAndVersion(any(), eq("0.2.0"))).thenReturn(Optional.empty()); when(skillVersionRepository.save(any(SkillVersion.class))).thenAnswer(invocation -> { SkillVersion saved = invocation.getArgument(0); @@ -205,7 +207,8 @@ class SkillPublishServiceTest { when(skillPackageValidator.validate(entries)).thenReturn(ValidationResult.pass()); when(skillMetadataParser.parse(skillMdContent)).thenReturn(metadata); when(prePublishValidator.validate(any())).thenReturn(ValidationResult.pass()); - when(skillRepository.findByNamespaceIdAndSlug(any(), eq("auto-skill"))).thenReturn(Optional.of(skill)); + when(skillRepository.findByNamespaceIdAndSlug(any(), eq("auto-skill"))).thenReturn(List.of(skill)); + when(skillRepository.findByNamespaceIdAndSlugAndOwnerId(any(), eq("auto-skill"), eq(publisherId))).thenReturn(Optional.of(skill)); when(skillVersionRepository.findBySkillIdAndVersion(any(), eq("1.0.0"))).thenReturn(Optional.empty()); when(skillVersionRepository.save(any(SkillVersion.class))).thenAnswer(invocation -> { SkillVersion saved = invocation.getArgument(0); @@ -253,7 +256,8 @@ class SkillPublishServiceTest { when(skillPackageValidator.validate(entries)).thenReturn(ValidationResult.pass()); when(skillMetadataParser.parse(skillMdContent)).thenReturn(metadata); when(prePublishValidator.validate(any())).thenReturn(ValidationResult.pass()); - when(skillRepository.findByNamespaceIdAndSlug(any(), eq("test-skill"))).thenReturn(Optional.of(archivedSkill)); + when(skillRepository.findByNamespaceIdAndSlug(any(), eq("test-skill"))).thenReturn(List.of(archivedSkill)); + when(skillRepository.findByNamespaceIdAndSlugAndOwnerId(any(), eq("test-skill"), eq(publisherId))).thenReturn(Optional.of(archivedSkill)); assertThrows(DomainBadRequestException.class, () -> service.publishFromEntries( namespaceSlug, @@ -283,7 +287,8 @@ class SkillPublishServiceTest { when(skillPackageValidator.validate(entries)).thenReturn(ValidationResult.pass()); when(skillMetadataParser.parse(skillMdContent)).thenReturn(metadata); when(prePublishValidator.validate(any())).thenReturn(ValidationResult.pass()); - when(skillRepository.findByNamespaceIdAndSlug(any(), eq("test-skill"))).thenReturn(Optional.of(new Skill(1L, "test-skill", publisherId, SkillVisibility.PUBLIC))); + when(skillRepository.findByNamespaceIdAndSlug(any(), eq("test-skill"))).thenReturn(List.of(new Skill(1L, "test-skill", publisherId, SkillVisibility.PUBLIC))); + when(skillRepository.findByNamespaceIdAndSlugAndOwnerId(any(), eq("test-skill"), eq(publisherId))).thenReturn(Optional.of(new Skill(1L, "test-skill", publisherId, SkillVisibility.PUBLIC))); when(skillVersionRepository.findBySkillIdAndVersion(any(), anyString())).thenReturn(Optional.empty()); when(skillVersionRepository.save(any(SkillVersion.class))).thenAnswer(invocation -> { SkillVersion saved = invocation.getArgument(0); @@ -367,7 +372,8 @@ class SkillPublishServiceTest { when(skillPackageValidator.validate(entries)).thenReturn(ValidationResult.pass()); when(skillMetadataParser.parse(skillMdContent)).thenReturn(metadata); when(prePublishValidator.validate(any())).thenReturn(ValidationResult.pass()); - when(skillRepository.findByNamespaceIdAndSlug(any(), eq("admin-skill"))).thenReturn(Optional.of(skill)); + when(skillRepository.findByNamespaceIdAndSlug(any(), eq("admin-skill"))).thenReturn(List.of(skill)); + when(skillRepository.findByNamespaceIdAndSlugAndOwnerId(any(), eq("admin-skill"), eq(publisherId))).thenReturn(Optional.of(skill)); when(skillVersionRepository.findBySkillIdAndVersion(any(), eq("1.0.0"))).thenReturn(Optional.empty()); when(skillVersionRepository.save(any(SkillVersion.class))).thenAnswer(invocation -> { SkillVersion saved = invocation.getArgument(0); @@ -413,7 +419,8 @@ class SkillPublishServiceTest { Skill skill = new Skill(namespace.getId(), "too-long-skill", publisherId, SkillVisibility.PUBLIC); setId(skill, 10L); - when(skillRepository.findByNamespaceIdAndSlug(namespace.getId(), "too-long-skill")).thenReturn(Optional.of(skill)); + when(skillRepository.findByNamespaceIdAndSlug(namespace.getId(), "too-long-skill")).thenReturn(List.of(skill)); + when(skillRepository.findByNamespaceIdAndSlugAndOwnerId(namespace.getId(), "too-long-skill", publisherId)).thenReturn(Optional.of(skill)); when(skillVersionRepository.findBySkillIdAndVersion(skill.getId(), "1.0.0")).thenReturn(Optional.empty()); when(skillVersionRepository.save(any())).thenAnswer(invocation -> { SkillVersion version = invocation.getArgument(0); @@ -537,6 +544,129 @@ class SkillPublishServiceTest { )); } + @Test + void testPublishFromEntries_ShouldRejectWhenOtherOwnerHasPublishedSkill() throws Exception { + String namespaceSlug = "test-ns"; + String publisherId = "user-200"; + String skillMdContent = "---\nname: test-skill\ndescription: Test\nversion: 1.0.0\n---\nBody"; + + PackageEntry skillMd = new PackageEntry("SKILL.md", skillMdContent.getBytes(), skillMdContent.length(), "text/markdown"); + List entries = List.of(skillMd); + + Namespace namespace = new Namespace(namespaceSlug, "Test NS", "user-1"); + setId(namespace, 1L); + NamespaceMember member = mock(NamespaceMember.class); + SkillMetadata metadata = new SkillMetadata("test-skill", "Test", "1.0.0", "Body", Map.of()); + + // Existing skill owned by another user with a published version + Skill existingSkill = new Skill(1L, "test-skill", "user-100", SkillVisibility.PUBLIC); + setId(existingSkill, 1L); + SkillVersion publishedVersion = new SkillVersion(1L, "0.1.0", "user-100"); + publishedVersion.setStatus(SkillVersionStatus.PUBLISHED); + + when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace)); + when(namespaceMemberRepository.findByNamespaceIdAndUserId(any(), eq(publisherId))).thenReturn(Optional.of(member)); + when(skillPackageValidator.validate(entries)).thenReturn(ValidationResult.pass()); + when(skillMetadataParser.parse(skillMdContent)).thenReturn(metadata); + when(prePublishValidator.validate(any())).thenReturn(ValidationResult.pass()); + when(skillRepository.findByNamespaceIdAndSlug(any(), eq("test-skill"))).thenReturn(List.of(existingSkill)); + when(skillVersionRepository.findBySkillIdAndStatus(1L, SkillVersionStatus.PUBLISHED)).thenReturn(List.of(publishedVersion)); + + assertThrows(DomainBadRequestException.class, () -> service.publishFromEntries( + namespaceSlug, entries, publisherId, SkillVisibility.PUBLIC, Set.of() + )); + } + + @Test + void testPublishFromEntries_ShouldAllowWhenOtherOwnerHasNonPublishedSkill() throws Exception { + String namespaceSlug = "test-ns"; + String publisherId = "user-200"; + String skillMdContent = "---\nname: test-skill\ndescription: Test\nversion: 1.0.0\n---\nBody"; + + PackageEntry skillMd = new PackageEntry("SKILL.md", skillMdContent.getBytes(), skillMdContent.length(), "text/markdown"); + List entries = List.of(skillMd); + + Namespace namespace = new Namespace(namespaceSlug, "Test NS", "user-1"); + setId(namespace, 1L); + NamespaceMember member = mock(NamespaceMember.class); + SkillMetadata metadata = new SkillMetadata("test-skill", "Test", "1.0.0", "Body", Map.of()); + + // Existing skill owned by another user with NO published version + Skill existingSkill = new Skill(1L, "test-skill", "user-100", SkillVisibility.PUBLIC); + setId(existingSkill, 1L); + + Skill newSkill = new Skill(1L, "test-skill", publisherId, SkillVisibility.PUBLIC); + setId(newSkill, 2L); + + when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace)); + when(namespaceMemberRepository.findByNamespaceIdAndUserId(any(), eq(publisherId))).thenReturn(Optional.of(member)); + when(skillPackageValidator.validate(entries)).thenReturn(ValidationResult.pass()); + when(skillMetadataParser.parse(skillMdContent)).thenReturn(metadata); + when(prePublishValidator.validate(any())).thenReturn(ValidationResult.pass()); + when(skillRepository.findByNamespaceIdAndSlug(any(), eq("test-skill"))).thenReturn(List.of(existingSkill)); + when(skillVersionRepository.findBySkillIdAndStatus(1L, SkillVersionStatus.PUBLISHED)).thenReturn(List.of()); + when(skillRepository.findByNamespaceIdAndSlugAndOwnerId(any(), eq("test-skill"), eq(publisherId))).thenReturn(Optional.empty()); + when(skillRepository.save(any(Skill.class))).thenReturn(newSkill); + when(skillVersionRepository.findBySkillIdAndVersion(any(), eq("1.0.0"))).thenReturn(Optional.empty()); + when(skillVersionRepository.save(any(SkillVersion.class))).thenAnswer(invocation -> { + SkillVersion saved = invocation.getArgument(0); + if (saved.getId() == null) setId(saved, 10L); + return saved; + }); + + SkillPublishService.PublishResult result = service.publishFromEntries( + namespaceSlug, entries, publisherId, SkillVisibility.PUBLIC, Set.of() + ); + + assertNotNull(result); + assertEquals("test-skill", result.slug()); + } + + @Test + void testPublishFromEntries_ShouldAutoWithdrawPendingVersions() throws Exception { + String namespaceSlug = "test-ns"; + String publisherId = "user-100"; + String skillMdContent = "---\nname: test-skill\ndescription: Test\nversion: 2.0.0\n---\nBody"; + + PackageEntry skillMd = new PackageEntry("SKILL.md", skillMdContent.getBytes(), skillMdContent.length(), "text/markdown"); + List entries = List.of(skillMd); + + Namespace namespace = new Namespace(namespaceSlug, "Test NS", "user-1"); + setId(namespace, 1L); + NamespaceMember member = mock(NamespaceMember.class); + SkillMetadata metadata = new SkillMetadata("test-skill", "Test", "2.0.0", "Body", Map.of()); + + Skill skill = new Skill(1L, "test-skill", publisherId, SkillVisibility.PUBLIC); + setId(skill, 1L); + + // Existing pending version + SkillVersion pendingV1 = new SkillVersion(1L, "1.0.0", publisherId); + pendingV1.setStatus(SkillVersionStatus.PENDING_REVIEW); + setId(pendingV1, 5L); + + when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace)); + when(namespaceMemberRepository.findByNamespaceIdAndUserId(any(), eq(publisherId))).thenReturn(Optional.of(member)); + when(skillPackageValidator.validate(entries)).thenReturn(ValidationResult.pass()); + when(skillMetadataParser.parse(skillMdContent)).thenReturn(metadata); + when(prePublishValidator.validate(any())).thenReturn(ValidationResult.pass()); + when(skillRepository.findByNamespaceIdAndSlug(any(), eq("test-skill"))).thenReturn(List.of(skill)); + when(skillRepository.findByNamespaceIdAndSlugAndOwnerId(any(), eq("test-skill"), eq(publisherId))).thenReturn(Optional.of(skill)); + when(skillVersionRepository.findBySkillIdAndStatus(1L, SkillVersionStatus.PENDING_REVIEW)).thenReturn(List.of(pendingV1)); + when(skillVersionRepository.findBySkillIdAndVersion(any(), eq("2.0.0"))).thenReturn(Optional.empty()); + when(skillVersionRepository.save(any(SkillVersion.class))).thenAnswer(invocation -> { + SkillVersion saved = invocation.getArgument(0); + if (saved.getId() == null) setId(saved, 10L); + return saved; + }); + when(skillRepository.save(any())).thenReturn(skill); + + service.publishFromEntries(namespaceSlug, entries, publisherId, SkillVisibility.PUBLIC, Set.of()); + + // Verify pending version was withdrawn to DRAFT + assertEquals(SkillVersionStatus.DRAFT, pendingV1.getStatus()); + verify(skillVersionRepository).save(pendingV1); + } + private void setId(Object entity, Long id) throws Exception { Field idField = entity.getClass().getDeclaredField("id"); idField.setAccessible(true); diff --git a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillQueryServiceTest.java b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillQueryServiceTest.java index 79866b58..79f9e611 100644 --- a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillQueryServiceTest.java +++ b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillQueryServiceTest.java @@ -87,7 +87,7 @@ class SkillQueryServiceTest { setId(version, 10L); when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace)); - when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill)); + when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill)); when(visibilityChecker.canAccess(skill, userId, userNsRoles)).thenReturn(true); when(skillVersionRepository.findById(10L)).thenReturn(Optional.of(version)); @@ -115,7 +115,7 @@ class SkillQueryServiceTest { setId(skill, 1L); when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace)); - when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill)); + when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill)); when(visibilityChecker.canAccess(skill, userId, userNsRoles)).thenReturn(false); // Act & Assert @@ -189,7 +189,7 @@ class SkillQueryServiceTest { Map userNsRoles = Map.of(1L, NamespaceRole.MEMBER); when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace)); - when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill)); + when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill)); when(visibilityChecker.canAccess(skill, "user-100", userNsRoles)).thenReturn(true); when(skillVersionRepository.findBySkillIdAndVersion(1L, version)).thenReturn(Optional.of(skillVersion)); when(skillFileRepository.findByVersionId(1L)).thenReturn(List.of(file1)); @@ -219,7 +219,7 @@ class SkillQueryServiceTest { skillVersion.setStatus(SkillVersionStatus.DRAFT); when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace)); - when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill)); + when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill)); when(visibilityChecker.canAccess(skill, "user-100", userNsRoles)).thenReturn(true); when(skillVersionRepository.findBySkillIdAndVersion(1L, version)).thenReturn(Optional.of(skillVersion)); @@ -246,7 +246,7 @@ class SkillQueryServiceTest { SkillFile file = new SkillFile(1L, filePath, 100L, "text/markdown", "hash1", "skills/1/1/SKILL.md"); when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace)); - when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill)); + when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill)); when(visibilityChecker.canAccess(skill, "user-100", userNsRoles)).thenReturn(true); when(skillVersionRepository.findBySkillIdAndVersion(1L, version)).thenReturn(Optional.of(skillVersion)); when(skillFileRepository.findByVersionId(1L)).thenReturn(List.of(file)); @@ -279,7 +279,7 @@ class SkillQueryServiceTest { skillVersion.setManifestJson("[{\"path\":\"SKILL.md\"}]"); when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace)); - when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill)); + when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill)); when(visibilityChecker.canAccess(skill, "user-100", userNsRoles)).thenReturn(true); when(skillVersionRepository.findBySkillIdAndVersion(1L, version)).thenReturn(Optional.of(skillVersion)); @@ -313,7 +313,7 @@ class SkillQueryServiceTest { SkillFile file = new SkillFile(11L, "README.md", 12L, "text/markdown", "hash", "storage-key"); when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace)); - when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill)); + when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill)); when(visibilityChecker.canAccess(skill, "user-100", userNsRoles)).thenReturn(true); when(skillVersionRepository.findById(11L)).thenReturn(Optional.of(latestVersion)); when(skillFileRepository.findByVersionId(11L)).thenReturn(List.of(file)); @@ -351,7 +351,7 @@ class SkillQueryServiceTest { rejected.setStatus(SkillVersionStatus.REJECTED); when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace)); - when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill)); + when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill)); when(visibilityChecker.canAccess(skill, ownerId, userNsRoles)).thenReturn(true); when(skillVersionRepository.findBySkillId(1L)).thenReturn(List.of(pending, published, rejected)); @@ -385,7 +385,7 @@ class SkillQueryServiceTest { SkillFile version110File = new SkillFile(10L, "SKILL.md", 10L, "text/markdown", "hash110", "key110"); when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace)); - when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill)); + when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill)); when(visibilityChecker.canAccess(skill, "user-100", userNsRoles)).thenReturn(true); when(skillVersionRepository.findBySkillIdAndStatus(1L, SkillVersionStatus.PUBLISHED)) .thenReturn(List.of(version100, version110)); @@ -427,7 +427,7 @@ class SkillQueryServiceTest { SkillFile file = new SkillFile(11L, "SKILL.md", 10L, "text/markdown", "hash", "key"); when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace)); - when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill)); + when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill)); when(visibilityChecker.canAccess(skill, null, userNsRoles)).thenReturn(true); when(skillVersionRepository.findById(11L)).thenReturn(Optional.of(version)); when(skillVersionRepository.findBySkillIdAndStatus(3L, SkillVersionStatus.PUBLISHED)).thenReturn(List.of(version)); @@ -460,7 +460,7 @@ class SkillQueryServiceTest { skill.setStatus(SkillStatus.ACTIVE); when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace)); - when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill)); + when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill)); when(visibilityChecker.canAccess(skill, userId, userNsRoles)).thenReturn(true); SkillQueryService.SkillDetailDTO result = service.getSkillDetail(namespaceSlug, skillSlug, userId, userNsRoles); @@ -574,7 +574,7 @@ class SkillQueryServiceTest { skill.setStatus(SkillStatus.ACTIVE); when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace)); - when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill)); + when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill)); when(visibilityChecker.canAccess(skill, userId, userNsRoles)).thenReturn(true); SkillQueryService.SkillDetailDTO result = service.getSkillDetail(namespaceSlug, skillSlug, userId, userNsRoles); @@ -607,7 +607,7 @@ class SkillQueryServiceTest { pending.setStatus(SkillVersionStatus.PENDING_REVIEW); when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace)); - when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill)); + when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill)); when(visibilityChecker.canAccess(skill, ownerId, userNsRoles)).thenReturn(true); when(skillVersionRepository.findBySkillIdAndStatus(1L, SkillVersionStatus.PENDING_REVIEW)) .thenReturn(List.of(pending)); @@ -642,7 +642,7 @@ class SkillQueryServiceTest { pending.setStatus(SkillVersionStatus.PENDING_REVIEW); when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace)); - when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill)); + when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill)); when(visibilityChecker.canAccess(skill, ownerId, userNsRoles)).thenReturn(true); when(skillVersionRepository.findById(11L)).thenReturn(Optional.of(published)); @@ -674,7 +674,7 @@ class SkillQueryServiceTest { pending.setManifestJson("[{\"path\":\"SKILL.md\"}]"); when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace)); - when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill)); + when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill)); when(visibilityChecker.canAccess(skill, ownerId, userNsRoles)).thenReturn(true); when(skillVersionRepository.findBySkillIdAndVersion(1L, version)).thenReturn(Optional.of(pending)); @@ -709,7 +709,7 @@ class SkillQueryServiceTest { SkillFile file = new SkillFile(11L, "README.md", 12L, "text/markdown", "hash", "storage-key"); when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace)); - when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill)); + when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill)); when(visibilityChecker.canAccess(skill, ownerId, userNsRoles)).thenReturn(true); when(skillVersionRepository.findBySkillIdAndVersion(1L, version)).thenReturn(Optional.of(pending)); when(skillFileRepository.findByVersionId(11L)).thenReturn(List.of(file)); @@ -739,7 +739,7 @@ class SkillQueryServiceTest { pending.setStatus(SkillVersionStatus.PENDING_REVIEW); when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace)); - when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill)); + when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill)); when(visibilityChecker.canAccess(skill, viewerId, userNsRoles)).thenReturn(true); when(skillVersionRepository.findBySkillIdAndVersion(1L, version)).thenReturn(Optional.of(pending)); @@ -771,7 +771,7 @@ class SkillQueryServiceTest { rejected.setStatus(SkillVersionStatus.REJECTED); when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace)); - when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill)); + when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill)); when(visibilityChecker.canAccess(skill, userId, userNsRoles)).thenReturn(true); when(skillVersionRepository.findBySkillId(1L)).thenReturn(List.of(rejected, draft, published)); @@ -805,7 +805,7 @@ class SkillQueryServiceTest { published.setStatus(SkillVersionStatus.PUBLISHED); when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace)); - when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill)); + when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill)); when(visibilityChecker.canAccess(skill, userId, userNsRoles)).thenReturn(true); when(skillVersionRepository.findBySkillIdAndStatus(1L, SkillVersionStatus.PUBLISHED)).thenReturn(List.of(published)); diff --git a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillTagServiceTest.java b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillTagServiceTest.java index 223b24dc..32c1ee71 100644 --- a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillTagServiceTest.java +++ b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillTagServiceTest.java @@ -73,7 +73,7 @@ class SkillTagServiceTest { when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace)); when(namespaceMemberRepository.findByNamespaceIdAndUserId(1L, operatorId)) .thenReturn(Optional.of(new NamespaceMember(1L, operatorId, NamespaceRole.OWNER))); - when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill)); + when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill)); when(skillVersionRepository.findBySkillIdAndVersion(1L, targetVersion)).thenReturn(Optional.of(version)); when(skillTagRepository.findBySkillIdAndTagName(1L, tagName)).thenReturn(Optional.empty()); when(skillTagRepository.save(any())).thenReturn(tag); @@ -118,7 +118,7 @@ class SkillTagServiceTest { when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace)); when(namespaceMemberRepository.findByNamespaceIdAndUserId(1L, operatorId)) .thenReturn(Optional.of(new NamespaceMember(1L, operatorId, NamespaceRole.ADMIN))); - when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill)); + when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill)); when(skillTagRepository.findBySkillIdAndTagName(1L, tagName)).thenReturn(Optional.of(tag)); // Act @@ -175,7 +175,7 @@ class SkillTagServiceTest { SkillTag tag2 = new SkillTag(1L, "beta", 2L, "user-100"); when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace)); - when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill)); + when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill)); when(skillTagRepository.findBySkillId(1L)).thenReturn(List.of(tag1, tag2)); when(visibilityChecker.canAccess(eq(skill), isNull(), eq(java.util.Map.of()))).thenReturn(true); diff --git a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/JpaSkillRepositoryAdapter.java b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/JpaSkillRepositoryAdapter.java index cfe57ff3..4a941c24 100644 --- a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/JpaSkillRepositoryAdapter.java +++ b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/JpaSkillRepositoryAdapter.java @@ -38,10 +38,15 @@ public class JpaSkillRepositoryAdapter implements SkillRepository { } @Override - public Optional findByNamespaceIdAndSlug(Long namespaceId, String slug) { + public List findByNamespaceIdAndSlug(Long namespaceId, String slug) { return delegate.findByNamespaceIdAndSlug(namespaceId, slug); } + @Override + public Optional findByNamespaceIdAndSlugAndOwnerId(Long namespaceId, String slug, String ownerId) { + return delegate.findByNamespaceIdAndSlugAndOwnerId(namespaceId, slug, ownerId); + } + @Override public List findByNamespaceIdAndStatus(Long namespaceId, SkillStatus status) { return delegate.findByNamespaceIdAndStatus(namespaceId, status); diff --git a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/SkillJpaRepository.java b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/SkillJpaRepository.java index 6cba15a8..962ae78d 100644 --- a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/SkillJpaRepository.java +++ b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/SkillJpaRepository.java @@ -18,7 +18,8 @@ import java.util.Optional; @Repository public interface SkillJpaRepository extends JpaRepository, SkillRepository { List findByIdIn(List ids); - Optional findByNamespaceIdAndSlug(Long namespaceId, String slug); + List findByNamespaceIdAndSlug(Long namespaceId, String slug); + Optional findByNamespaceIdAndSlugAndOwnerId(Long namespaceId, String slug, String ownerId); @Override default List findByNamespaceIdAndStatus(Long namespaceId, SkillStatus status) { diff --git a/web/src/shared/hooks/use-skill-queries.ts b/web/src/shared/hooks/use-skill-queries.ts index 1a4d3a44..ab4da559 100644 --- a/web/src/shared/hooks/use-skill-queries.ts +++ b/web/src/shared/hooks/use-skill-queries.ts @@ -246,7 +246,7 @@ export function usePublishSkill() { skipGlobalErrorHandler: true, }, onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['skills', 'my'] }) + queryClient.invalidateQueries({ queryKey: ['skills'] }) }, }) }