fix(domain): hide stale rejected preview when newer version published

When a skill had an earlier REJECTED version and a later PUBLISHED
version, the my-skills list showed both "已发布" and "已拒绝" badges
because ownerPreviewVersion pointed to the stale rejected version.

Changed SkillLifecycleProjectionService to filter out non-published
versions older than the published one. Only versions newer than the
published version are surfaced as the owner preview, treating older
rejected attempts as settled history.

- Refactored projectForViewer and projectForOwnerSummary to use new
  buildProjection helper
- Replaced resolveOwnerPendingPreview and resolveNewestNonPublishedVersion
  with resolveNewerNonPublishedVersion that filters by recency
- Added test case covering the stale rejected scenario

Synced from SAAS commit c9eefa93fix(domain): hide stale rejected preview when newer version published
This commit is contained in:
PR Review Helper 2026-06-05 10:52:37 +08:00
parent 90fc97e740
commit ed6ea2417a
2 changed files with 50 additions and 29 deletions

View file

@ -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);

View file

@ -39,6 +39,10 @@ public class SkillLifecycleProjectionService {
ResolutionMode resolutionMode
) {}
private static final Comparator<SkillVersion> 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<Long, NamespaceRole> 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<Long, NamespaceRole> 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<SkillVersion> 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);
}