From 4ad97aa55a0c337ec11eb8137513eec5d3c847e6 Mon Sep 17 00:00:00 2001 From: yun-zhi-ztl <15071461069@163.com> Date: Sun, 15 Mar 2026 19:45:10 +0800 Subject: [PATCH] feat(skill): add version management APIs --- .../portal/SkillLifecycleController.java | 40 +++++++++++++++++ .../dto/SkillVersionRereleaseRequest.java | 9 ++++ .../portal/SkillLifecycleControllerTest.java | 43 +++++++++++++++++++ .../skill/service/SkillQueryService.java | 13 +++++- .../skill/service/SkillQueryServiceTest.java | 37 ++++++++++++++++ 5 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/SkillVersionRereleaseRequest.java 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 d5f23324..7bb2964c 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 @@ -12,10 +12,13 @@ import com.iflytek.skillhub.domain.skill.SkillRepository; import com.iflytek.skillhub.domain.skill.SkillVersion; import com.iflytek.skillhub.domain.skill.SkillVersionRepository; import com.iflytek.skillhub.domain.skill.service.SkillGovernanceService; +import com.iflytek.skillhub.domain.skill.service.SkillPublishService; import com.iflytek.skillhub.dto.AdminSkillActionRequest; import com.iflytek.skillhub.dto.ApiResponse; import com.iflytek.skillhub.dto.ApiResponseFactory; import com.iflytek.skillhub.dto.SkillLifecycleMutationResponse; +import com.iflytek.skillhub.dto.SkillVersionRereleaseRequest; +import jakarta.validation.Valid; import jakarta.servlet.http.HttpServletRequest; import java.util.Map; import org.springframework.web.bind.annotation.DeleteMapping; @@ -35,6 +38,7 @@ public class SkillLifecycleController extends BaseApiController { private final SkillVersionRepository skillVersionRepository; private final SkillGovernanceService skillGovernanceService; private final ReviewService reviewService; + private final SkillPublishService skillPublishService; private final AuditLogService auditLogService; public SkillLifecycleController(NamespaceRepository namespaceRepository, @@ -42,6 +46,7 @@ public class SkillLifecycleController extends BaseApiController { SkillVersionRepository skillVersionRepository, SkillGovernanceService skillGovernanceService, ReviewService reviewService, + SkillPublishService skillPublishService, AuditLogService auditLogService, ApiResponseFactory responseFactory) { super(responseFactory); @@ -50,6 +55,7 @@ public class SkillLifecycleController extends BaseApiController { this.skillVersionRepository = skillVersionRepository; this.skillGovernanceService = skillGovernanceService; this.reviewService = reviewService; + this.skillPublishService = skillPublishService; this.auditLogService = auditLogService; } @@ -141,6 +147,40 @@ public class SkillLifecycleController extends BaseApiController { new SkillLifecycleMutationResponse(skill.getId(), skillVersion.getId(), "WITHDRAW_REVIEW", "DELETED")); } + @PostMapping("/{namespace}/{slug}/versions/{version}/rerelease") + public ApiResponse rereleaseVersion(@PathVariable String namespace, + @PathVariable String slug, + @PathVariable String version, + @Valid @RequestBody SkillVersionRereleaseRequest request, + @RequestAttribute("userId") String userId, + @RequestAttribute(value = "userNsRoles", required = false) Map userNsRoles, + HttpServletRequest httpRequest) { + Skill skill = findSkill(namespace, slug); + SkillVersion skillVersion = skillVersionRepository.findBySkillIdAndVersion(skill.getId(), version) + .orElseThrow(() -> new DomainBadRequestException("error.skill.version.notFound", version)); + SkillPublishService.PublishResult result = skillPublishService.rereleasePublishedVersion( + skill.getId(), + skillVersion.getVersion(), + request.targetVersion().trim(), + userId, + userNsRoles != null ? userNsRoles : Map.of() + ); + auditLogService.record( + userId, + "RERELEASE_SKILL_VERSION", + "SKILL_VERSION", + skillVersion.getId(), + null, + httpRequest.getRemoteAddr(), + httpRequest.getHeader("User-Agent"), + "{\"sourceVersion\":\"" + version.replace("\"", "\\\"") + + "\",\"targetVersion\":\"" + request.targetVersion().trim().replace("\"", "\\\"") + "\"}" + ); + + return ok("response.success.updated", + new SkillLifecycleMutationResponse(result.skillId(), result.version().getId(), "RERELEASE_VERSION", result.version().getStatus().name())); + } + private Skill findSkill(String namespaceSlug, String skillSlug) { String cleanNamespace = namespaceSlug.startsWith("@") ? namespaceSlug.substring(1) : namespaceSlug; Namespace namespace = namespaceRepository.findBySlug(cleanNamespace) diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/SkillVersionRereleaseRequest.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/SkillVersionRereleaseRequest.java new file mode 100644 index 00000000..6ca9e708 --- /dev/null +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/SkillVersionRereleaseRequest.java @@ -0,0 +1,9 @@ +package com.iflytek.skillhub.dto; + +import jakarta.validation.constraints.NotBlank; + +public record SkillVersionRereleaseRequest( + @NotBlank(message = "{validation.required}") + String targetVersion +) { +} 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 40acf537..26613505 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 @@ -25,6 +25,7 @@ import com.iflytek.skillhub.domain.skill.SkillVersion; import com.iflytek.skillhub.domain.skill.SkillVersionRepository; import com.iflytek.skillhub.domain.skill.SkillVersionStatus; import com.iflytek.skillhub.domain.skill.SkillVisibility; +import com.iflytek.skillhub.domain.skill.service.SkillPublishService; import com.iflytek.skillhub.domain.skill.service.SkillGovernanceService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -60,6 +61,9 @@ class SkillLifecycleControllerTest { @MockBean private ReviewService reviewService; + @MockBean + private SkillPublishService skillPublishService; + @MockBean private AuditLogService auditLogService; @@ -173,6 +177,45 @@ class SkillLifecycleControllerTest { .andExpect(jsonPath("$.data.status").value("DELETED")); } + @Test + void rereleaseVersion_returnsUnifiedEnvelope() throws Exception { + Namespace namespace = new Namespace("global", "Global", "owner"); + setNamespaceId(namespace, 1L); + Skill skill = new Skill(1L, "demo-skill", "owner", SkillVisibility.PUBLIC); + setSkillId(skill, 1L); + SkillVersion newVersion = new SkillVersion(1L, "1.2.4", "owner"); + setSkillVersionId(newVersion, 3L); + 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)); + SkillVersion sourceVersion = new SkillVersion(1L, "1.2.3", "owner"); + setSkillVersionId(sourceVersion, 2L); + sourceVersion.setStatus(SkillVersionStatus.PUBLISHED); + given(skillVersionRepository.findBySkillIdAndVersion(1L, "1.2.3")).willReturn(java.util.Optional.of(sourceVersion)); + given(skillPublishService.rereleasePublishedVersion( + eq(1L), + eq("1.2.3"), + eq("1.2.4"), + eq("usr_1"), + anyMap())) + .willReturn(new SkillPublishService.PublishResult(1L, "demo-skill", newVersion)); + + mockMvc.perform(post("/api/web/skills/global/demo-skill/versions/1.2.3/rerelease") + .requestAttr("userId", "usr_1") + .requestAttr("userNsRoles", java.util.Map.of(1L, NamespaceRole.ADMIN)) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"targetVersion\":\"1.2.4\"}") + .with(user("usr_1")) + .with(csrf())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(0)) + .andExpect(jsonPath("$.data.skillId").value(1)) + .andExpect(jsonPath("$.data.versionId").value(3)) + .andExpect(jsonPath("$.data.action").value("RERELEASE_VERSION")) + .andExpect(jsonPath("$.data.status").value("PUBLISHED")); + } + private Skill skillWithStatus(Skill skill, com.iflytek.skillhub.domain.skill.SkillStatus status) { skill.setStatus(status); return skill; 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 938979cd..a371940d 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 @@ -256,8 +256,10 @@ public class SkillQueryService { if (canManageRestrictedSkill(skill, currentUserId, userNsRoles)) { visibleVersions = skillVersionRepository.findBySkillId(skill.getId()).stream() .filter(version -> version.getStatus() == SkillVersionStatus.PUBLISHED + || version.getStatus() == SkillVersionStatus.PENDING_REVIEW || version.getStatus() == SkillVersionStatus.DRAFT - || version.getStatus() == SkillVersionStatus.REJECTED) + || version.getStatus() == SkillVersionStatus.REJECTED + || version.getStatus() == SkillVersionStatus.YANKED) .sorted(Comparator .comparingInt((SkillVersion version) -> lifecycleListPriority(version.getStatus())) .thenComparing(SkillVersion::getPublishedAt, @@ -435,6 +437,15 @@ public class SkillQueryService { if (status == SkillVersionStatus.REJECTED) { return 1; } + if (status == SkillVersionStatus.PENDING_REVIEW) { + return 2; + } + if (status == SkillVersionStatus.DRAFT) { + return 3; + } + if (status == SkillVersionStatus.YANKED) { + return 4; + } return 2; } 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 00d4adef..2dfbaba6 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 @@ -266,6 +266,43 @@ class SkillQueryServiceTest { assertEquals("README.md", result.get(0).getFilePath()); } + @Test + void testListVersions_ShouldIncludePendingAndRejectedForLifecycleManagers() throws Exception { + String namespaceSlug = "test-ns"; + String skillSlug = "test-skill"; + String ownerId = "user-100"; + Map userNsRoles = Map.of(1L, NamespaceRole.OWNER); + + Namespace namespace = new Namespace(namespaceSlug, "Test NS", ownerId); + setId(namespace, 1L); + Skill skill = new Skill(1L, skillSlug, ownerId, SkillVisibility.PUBLIC); + setId(skill, 1L); + skill.setStatus(SkillStatus.ACTIVE); + + SkillVersion published = new SkillVersion(1L, "1.0.0", ownerId); + setId(published, 10L); + published.setStatus(SkillVersionStatus.PUBLISHED); + published.setPublishedAt(java.time.LocalDateTime.of(2026, 3, 1, 10, 0)); + + SkillVersion pending = new SkillVersion(1L, "1.1.0", ownerId); + setId(pending, 11L); + pending.setStatus(SkillVersionStatus.PENDING_REVIEW); + + SkillVersion rejected = new SkillVersion(1L, "1.2.0", ownerId); + setId(rejected, 12L); + rejected.setStatus(SkillVersionStatus.REJECTED); + + when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace)); + when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill)); + when(visibilityChecker.canAccess(skill, ownerId, userNsRoles)).thenReturn(true); + when(skillVersionRepository.findBySkillId(1L)).thenReturn(List.of(pending, published, rejected)); + + Page result = service.listVersions(namespaceSlug, skillSlug, ownerId, userNsRoles, PageRequest.of(0, 20)); + + assertEquals(List.of("1.0.0", "1.2.0", "1.1.0"), + result.getContent().stream().map(SkillVersion::getVersion).toList()); + } + @Test void testResolveVersion_ShouldReturnLatestWhenHashDoesNotMatch() throws Exception { String namespaceSlug = "test-ns";