Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
package com.iflytek.skillhub.config;

import com.iflytek.skillhub.domain.bundle.SkillBundleDraftService;
import com.iflytek.skillhub.domain.bundle.SkillBundleItemRepository;
import com.iflytek.skillhub.domain.bundle.SkillBundleRepository;
import com.iflytek.skillhub.domain.bundle.SkillBundleReviewService;
import com.iflytek.skillhub.domain.bundle.SkillBundleReviewTaskRepository;
import com.iflytek.skillhub.domain.bundle.SkillBundleVersionRepository;
import com.iflytek.skillhub.domain.skill.VisibilityChecker;
import com.iflytek.skillhub.domain.skill.metadata.SkillMetadataParser;
import com.iflytek.skillhub.domain.skill.validation.SkillPackageValidator;
Expand Down Expand Up @@ -41,4 +47,19 @@ public SkillPackageValidator skillPackageValidator(SkillMetadataParser skillMeta
public VisibilityChecker visibilityChecker() {
return new VisibilityChecker();
}

@Bean
public SkillBundleDraftService skillBundleDraftService(SkillBundleRepository bundleRepository,
SkillBundleVersionRepository versionRepository,
SkillBundleItemRepository itemRepository,
SkillBundleDraftService.SkillBundleItemSourceResolver resolver) {
return new SkillBundleDraftService(bundleRepository, versionRepository, itemRepository, resolver);
}

@Bean
public SkillBundleReviewService skillBundleReviewService(SkillBundleRepository bundleRepository,
SkillBundleVersionRepository versionRepository,
SkillBundleReviewTaskRepository reviewTaskRepository) {
return new SkillBundleReviewService(bundleRepository, versionRepository, reviewTaskRepository);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.iflytek.skillhub.controller.admin;

import com.iflytek.skillhub.controller.BaseApiController;
import com.iflytek.skillhub.dto.ApiResponse;
import com.iflytek.skillhub.dto.ApiResponseFactory;
import com.iflytek.skillhub.dto.bundle.SkillBundleReviewActionRequest;
import com.iflytek.skillhub.service.bundle.SkillBundleAppService;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestAttribute;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* Admin endpoints for skill bundle review queue.
*/
@RestController
@RequestMapping("/api/v1/skill-bundle-reviews")
@PreAuthorize("hasAnyRole('SKILL_ADMIN','SUPER_ADMIN','NAMESPACE_ADMIN','NAMESPACE_OWNER')")
public class SkillBundleReviewController extends BaseApiController {

private final SkillBundleAppService appService;

public SkillBundleReviewController(SkillBundleAppService appService, ApiResponseFactory responseFactory) {
super(responseFactory);
this.appService = appService;
}

@PostMapping("/{id}/approve")
public ApiResponse<Long> approve(@PathVariable Long id,
@RequestBody(required = false) SkillBundleReviewActionRequest body,
@RequestAttribute("userId") String userId) {
String comment = body == null ? null : body.comment();
return ok("response.success.updated", appService.approve(id, comment, userId).getId());
}

@PostMapping("/{id}/reject")
public ApiResponse<Long> reject(@PathVariable Long id,
@RequestBody(required = false) SkillBundleReviewActionRequest body,
@RequestAttribute("userId") String userId) {
String comment = body == null ? null : body.comment();
return ok("response.success.updated", appService.reject(id, comment, userId).getId());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.iflytek.skillhub.controller.portal;

import com.iflytek.skillhub.controller.BaseApiController;
import com.iflytek.skillhub.dto.ApiResponse;
import com.iflytek.skillhub.dto.ApiResponseFactory;
import com.iflytek.skillhub.dto.bundle.BuildSkillBundleDraftRequest;
import com.iflytek.skillhub.dto.bundle.SkillBundleDetailResponse;
import com.iflytek.skillhub.dto.bundle.SkillBundleVersionResponse;
import com.iflytek.skillhub.service.bundle.SkillBundleAppService;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestAttribute;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
* Portal endpoints for the skill bundle module: build draft, query detail, submit
* for review, and download accounting.
*/
@RestController
@RequestMapping("/api/v1/skill-bundles")
public class SkillBundleController extends BaseApiController {

private final SkillBundleAppService appService;

public SkillBundleController(SkillBundleAppService appService, ApiResponseFactory responseFactory) {
super(responseFactory);
this.appService = appService;
}

@PostMapping("/{namespace}/drafts/build")
public ApiResponse<SkillBundleVersionResponse> buildDraft(@PathVariable String namespace,
@Valid @RequestBody BuildSkillBundleDraftRequest request,
@RequestAttribute("userId") String userId) {
return ok("response.success.created", appService.buildDraft(namespace, request, userId));
}

@PostMapping("/{namespace}/{slug}/versions/{bundleVersionId}/submit-review")
public ApiResponse<Long> submitReview(@PathVariable String namespace,
@PathVariable String slug,
@PathVariable Long bundleVersionId,
@RequestAttribute("userId") String userId) {
return ok("response.success.created",
appService.submitForReview(bundleVersionId, userId).getId());
}

@GetMapping("/{namespace}/{slug}")
public ApiResponse<SkillBundleDetailResponse> getDetail(@PathVariable String namespace,
@PathVariable String slug,
@RequestParam(required = false) String version) {
return ok("response.success.read", appService.getDetail(namespace, slug, version));
}

@PostMapping("/{namespace}/{slug}/download")
public ApiResponse<Void> recordDownload(@PathVariable String namespace,
@PathVariable String slug) {
appService.incrementDownload(namespace, slug);
return ok("response.success.created", null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.iflytek.skillhub.dto.bundle;

import com.iflytek.skillhub.domain.bundle.SkillBundleType;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

import java.util.List;

/**
* Builds a draft skill bundle from existing platform skills (no zip upload).
*/
public record BuildSkillBundleDraftRequest(
@NotBlank @Size(max = 128) String slug,
@NotBlank @Size(max = 256) String displayName,
@NotBlank @Size(max = 32) String version,
@NotNull SkillBundleType type,
@NotBlank @Size(max = 512) String summary,
List<String> targetProjectTypes,
List<String> roleTags,
@NotNull List<DraftItemRequest> items,
List<Long> mediaIds
) {
public record DraftItemRequest(
@NotNull Long skillId,
@NotNull Long skillVersionId,
@NotBlank @Size(max = 512) String roleDescription,
boolean required,
@Min(0) int installOrder
) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.iflytek.skillhub.dto.bundle;

import com.iflytek.skillhub.domain.bundle.SkillBundle;
import com.iflytek.skillhub.domain.bundle.SkillBundleType;
import com.iflytek.skillhub.domain.bundle.SkillBundleVersion;
import com.iflytek.skillhub.domain.bundle.SkillBundleVersionStatus;

import java.math.BigDecimal;
import java.time.Instant;
import java.util.List;

public record SkillBundleDetailResponse(
Long id,
Long namespaceId,
String slug,
String displayName,
SkillBundleType type,
String summary,
long downloadCount,
int starCount,
BigDecimal ratingAvg,
int ratingCount,
int commentCount,
Long latestVersionId,
VersionView version,
List<ItemView> items,
Instant updatedAt
) {
public record VersionView(Long id, String version, SkillBundleVersionStatus status, Instant publishedAt) {
public static VersionView from(SkillBundleVersion v) {
return new VersionView(v.getId(), v.getVersion(), v.getStatus(), v.getPublishedAt());
}
}

public record ItemView(
Long skillId,
String namespaceSlug,
String skillSlug,
String displayName,
String version,
String roleDescription,
boolean required,
int installOrder,
String detailUrl
) {}

public static SkillBundleDetailResponse build(SkillBundle bundle,
SkillBundleVersion version,
List<ItemView> items) {
return new SkillBundleDetailResponse(
bundle.getId(), bundle.getNamespaceId(), bundle.getSlug(),
bundle.getDisplayName(), bundle.getBundleType(), bundle.getSummary(),
bundle.getDownloadCount(), bundle.getStarCount(),
bundle.getRatingAvg(), bundle.getRatingCount(), bundle.getCommentCount(),
bundle.getLatestVersionId(),
version == null ? null : VersionView.from(version),
items,
bundle.getUpdatedAt()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.iflytek.skillhub.dto.bundle;

import jakarta.validation.constraints.Size;

public record SkillBundleReviewActionRequest(
@Size(max = 1000) String comment
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.iflytek.skillhub.dto.bundle;

import com.iflytek.skillhub.domain.bundle.SkillBundleVersion;
import com.iflytek.skillhub.domain.bundle.SkillBundleVersionStatus;

import java.time.Instant;

public record SkillBundleVersionResponse(
Long bundleId,
Long bundleVersionId,
String version,
SkillBundleVersionStatus status,
Instant publishedAt
) {
public static SkillBundleVersionResponse from(SkillBundleVersion v) {
return new SkillBundleVersionResponse(
v.getBundleId(), v.getId(), v.getVersion(),
v.getStatus(), v.getPublishedAt());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.iflytek.skillhub.service.bundle;

import com.iflytek.skillhub.domain.bundle.SkillBundleDraftService;
import com.iflytek.skillhub.domain.bundle.SkillBundleException;
import com.iflytek.skillhub.domain.namespace.Namespace;
import com.iflytek.skillhub.domain.namespace.NamespaceRepository;
import com.iflytek.skillhub.domain.skill.Skill;
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.SkillVersionStatus;
import org.springframework.stereotype.Component;

/**
* Resolves bundle item snapshots from the existing skill aggregate. The snapshot
* is captured at draft-build time so the bundle stays reproducible.
*/
@Component
public class JpaSkillBundleItemSourceResolver implements SkillBundleDraftService.SkillBundleItemSourceResolver {

private final SkillRepository skillRepository;
private final SkillVersionRepository skillVersionRepository;
private final NamespaceRepository namespaceRepository;

public JpaSkillBundleItemSourceResolver(SkillRepository skillRepository,
SkillVersionRepository skillVersionRepository,
NamespaceRepository namespaceRepository) {
this.skillRepository = skillRepository;
this.skillVersionRepository = skillVersionRepository;
this.namespaceRepository = namespaceRepository;
}

@Override
public SkillBundleDraftService.SkillBundleItemSnapshot resolveRegistryItem(Long skillId, Long skillVersionId) {
Skill skill = skillRepository.findById(skillId)
.orElseThrow(() -> new SkillBundleException("error.skillBundle.item.skillNotFound"));
SkillVersion version = skillVersionRepository.findById(skillVersionId)
.orElseThrow(() -> new SkillBundleException("error.skillBundle.item.versionNotFound"));
if (version.getStatus() != SkillVersionStatus.PUBLISHED) {
throw new SkillBundleException("error.skillBundle.item.versionNotPublished");
}
Namespace namespace = namespaceRepository.findById(skill.getNamespaceId())
.orElseThrow(() -> new SkillBundleException("error.skillBundle.item.namespaceNotFound"));
return new SkillBundleDraftService.SkillBundleItemSnapshot(
namespace.getSlug(),
skill.getSlug(),
skill.getDisplayName(),
version.getVersion(),
skill.getSummary(),
version.getPublishedAt() != null ? version.getPublishedAt() : version.getCreatedAt()
);
}
}
Loading