diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/MySkillAppServiceTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/MySkillAppServiceTest.java index c4faf136..1bb04758 100644 --- a/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/MySkillAppServiceTest.java +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/MySkillAppServiceTest.java @@ -265,6 +265,27 @@ class MySkillAppServiceTest { assertThat(result.items().get(0).headlineVersion().status()).isEqualTo("REJECTED"); } + @Test + void listMySkills_hidesStaleRejectedVersionOlderThanPublished() { + Skill skill = createSkill(6L, 101L, "recovered-skill", "user-1"); + SkillVersion rejectedVersion = createVersion(6L, 60L, "1.0.0", SkillVersionStatus.REJECTED, "2026-03-15T09:30:00Z"); + SkillVersion publishedVersion = createVersion(6L, 61L, "2.0.0", SkillVersionStatus.PUBLISHED, "2026-03-16T09:30:00Z"); + + given(skillRepository.findByOwnerId("user-1", PageRequest.of(0, 10))) + .willReturn(new PageImpl<>(List.of(skill), PageRequest.of(0, 10), 1)); + given(skillVersionRepository.findBySkillId(6L)).willReturn(List.of(rejectedVersion, publishedVersion)); + given(skillVersionRepository.findBySkillIdAndStatus(6L, SkillVersionStatus.PUBLISHED)) + .willReturn(List.of(publishedVersion)); + given(namespaceRepository.findByIdIn(List.of(101L))).willReturn(List.of(namespace(101L, "team-ai"))); + + var result = service.listMySkills("user-1", 0, 10); + + assertThat(result.items()).hasSize(1); + assertThat(result.items().get(0).headlineVersion().status()).isEqualTo("PUBLISHED"); + assertThat(result.items().get(0).headlineVersion().version()).isEqualTo("2.0.0"); + assertThat(result.items().get(0).ownerPreviewVersion()).isNull(); + } + private Skill createSkill(Long id, Long namespaceId, String slug, String ownerId) { Skill skill = new Skill(namespaceId, slug, ownerId, SkillVisibility.PUBLIC); skill.setDisplayName(slug); diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillLifecycleProjectionService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillLifecycleProjectionService.java index 711ff60d..6c9c4ce5 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillLifecycleProjectionService.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillLifecycleProjectionService.java @@ -39,6 +39,10 @@ public class SkillLifecycleProjectionService { ResolutionMode resolutionMode ) {} + private static final Comparator RECENCY = Comparator + .comparing(SkillVersion::getCreatedAt, Comparator.nullsLast(Comparator.naturalOrder())) + .thenComparing(SkillVersion::getId, Comparator.nullsLast(Comparator.naturalOrder())); + private final SkillVersionRepository skillVersionRepository; public SkillLifecycleProjectionService(SkillVersionRepository skillVersionRepository) { @@ -46,22 +50,26 @@ public class SkillLifecycleProjectionService { } public Projection projectForViewer(Skill skill, String currentUserId, Map userNsRoles) { - VersionProjection publishedVersion = toProjection(resolvePublishedVersion(skill)); - VersionProjection ownerPreviewVersion = toProjection(resolveOwnerPendingPreview(skill, currentUserId, userNsRoles)); - VersionProjection headlineVersion = publishedVersion != null ? publishedVersion : ownerPreviewVersion; - ResolutionMode resolutionMode = headlineVersion == null - ? ResolutionMode.NONE - : publishedVersion != null ? ResolutionMode.PUBLISHED : ResolutionMode.OWNER_PREVIEW; - return new Projection(headlineVersion, publishedVersion, ownerPreviewVersion, resolutionMode); + SkillVersion published = resolvePublishedVersion(skill); + SkillVersion preview = canManage(skill, currentUserId, userNsRoles) + ? resolveNewerNonPublishedVersion(skill, published) + : null; + return buildProjection(published, preview); } public Projection projectForOwnerSummary(Skill skill) { - VersionProjection publishedVersion = toProjection(resolvePublishedVersion(skill)); - VersionProjection ownerPreviewVersion = toProjection(resolveNewestNonPublishedVersion(skill)); + SkillVersion published = resolvePublishedVersion(skill); + SkillVersion preview = resolveNewerNonPublishedVersion(skill, published); + return buildProjection(published, preview); + } + + private Projection buildProjection(SkillVersion published, SkillVersion preview) { + VersionProjection publishedVersion = toProjection(published); + VersionProjection ownerPreviewVersion = toProjection(preview); VersionProjection headlineVersion = publishedVersion != null ? publishedVersion : ownerPreviewVersion; - ResolutionMode resolutionMode = headlineVersion == null - ? ResolutionMode.NONE - : publishedVersion != null ? ResolutionMode.PUBLISHED : ResolutionMode.OWNER_PREVIEW; + ResolutionMode resolutionMode = published != null ? ResolutionMode.PUBLISHED + : preview != null ? ResolutionMode.OWNER_PREVIEW + : ResolutionMode.NONE; return new Projection(headlineVersion, publishedVersion, ownerPreviewVersion, resolutionMode); } @@ -116,27 +124,19 @@ public class SkillLifecycleProjectionService { } /** - * Returns the newest non-published version the owner can preview. - * Includes PENDING_REVIEW, REJECTED, DRAFT, SCANNING, SCAN_FAILED — any status - * that isn't already covered by the published projection and isn't yanked. + * Returns the newest non-published version (PENDING_REVIEW, REJECTED, DRAFT, SCANNING, + * SCAN_FAILED) that represents a NEW round of work layered on top of the current published + * version. A non-published version that is older than the published version is treated as + * settled history (e.g. an early rejected attempt later superseded by a published release) + * and is intentionally not surfaced, so the owner does not see a stale preview/rejected badge + * next to an already-published skill. */ - private SkillVersion resolveOwnerPendingPreview(Skill skill, String currentUserId, Map userNsRoles) { - if (!canManage(skill, currentUserId, userNsRoles)) { - return null; - } + private SkillVersion resolveNewerNonPublishedVersion(Skill skill, SkillVersion publishedVersion) { return skillVersionRepository.findBySkillId(skill.getId()).stream() - .filter(v -> v.getStatus() != SkillVersionStatus.PUBLISHED - && v.getStatus() != SkillVersionStatus.YANKED) - .max(versionComparator()) - .orElse(null); - } - - private SkillVersion resolveNewestNonPublishedVersion(Skill skill) { - List versions = skillVersionRepository.findBySkillId(skill.getId()); - return versions.stream() .filter(version -> version.getStatus() != SkillVersionStatus.PUBLISHED && version.getStatus() != SkillVersionStatus.YANKED) - .max(versionComparator()) + .filter(version -> publishedVersion == null || RECENCY.compare(version, publishedVersion) > 0) + .max(RECENCY) .orElse(null); }