feat(skill-bundle): skill bundle aggregate, draft build, review and d…#448
feat(skill-bundle): skill bundle aggregate, draft build, review and d…#448paradoxgacsd wants to merge 1 commit into
Conversation
…etail Adds the skill bundle module described in the design doc: - V42 migration: skill_bundle aggregate, skill_bundle_version with manifest/lock JSON, skill_bundle_item snapshots, validation results, review tasks, social tables (star/rating/comment/download_event), and search index. - Domain layer: SkillBundle, SkillBundleVersion, SkillBundleItem, SkillBundleReviewTask aggregates with status enums, draft service enforcing item uniqueness / project-type and role-tag rules / registry version snapshotting, review service driving the DRAFT -> PENDING_REVIEW -> PUBLISHED|REJECTED state machine with optimistic-lock and self-review guards. - Infra layer: JPA repositories with paged queries and bulk updates. - App layer: JpaSkillBundleItemSourceResolver pulls published skill metadata; SkillBundleAppService converts payloads, resolves the visible version, and exposes detail and download endpoints. - HTTP: /api/v1/skill-bundles (build draft, get detail, submit review, download accounting) and admin /api/v1/skill-bundle-reviews (approve / reject). - Frontend: skill-bundle-detail page, react-query hooks and a thin envelope-aware fetch wrapper. - Tests: domain-level tests for draft validation and review state machine, vitest coverage for the front-end fetch wrapper.
There was a problem hiding this comment.
Code Review
This pull request introduces a new skill bundle management module, including domain models, database schema, application services, and API endpoints for building, reviewing, and managing skill bundles. The review identified several critical issues: the version sorting logic in SkillBundleAppService produces inconsistent magnitudes for different version lengths, the manifestJson and lockJson are currently hardcoded as empty objects, the buildDraft service suffers from an N+1 query performance issue during item resolution, and the submitForReview method fails to update audit fields when re-submitting previously rejected tasks.
| private long parseVersionSort(String version) { | ||
| // semver MAJOR.MINOR.PATCH packed into a sortable long; pre-release parts ignored. | ||
| String[] parts = version.split("[.+-]"); | ||
| long sort = 0; | ||
| for (int i = 0; i < Math.min(3, parts.length); i++) { | ||
| try { | ||
| sort = sort * 1_000_000L + Long.parseLong(parts[i]); | ||
| } catch (NumberFormatException ignored) { | ||
| break; | ||
| } | ||
| } | ||
| return sort; | ||
| } |
There was a problem hiding this comment.
parseVersionSort 的逻辑在处理不同段数的版本号时存在缺陷。由于循环次数受 parts.length 限制,导致 1.2 计算出的 sort 值(约 10^6)与 1.2.0 计算出的值(约 10^12)在数量级上不一致,从而导致排序失效(例如 1.2 会排在 1.1.9 之前)。建议无论版本号段数多少,都固定进行三次位移以保证数量级一致。
private long parseVersionSort(String version) {
String[] parts = version.split("[.+-]");
long sort = 0;
for (int i = 0; i < 3; i++) {
long val = 0;
if (i < parts.length) {
try {
val = Long.parseLong(parts[i]);
} catch (NumberFormatException ignored) {
break;
}
}
sort = sort * 1_000_000L + val;
}
return sort;
}| request.items().stream().map(item -> new SkillBundleDraftService.DraftItem( | ||
| item.skillId(), item.skillVersionId(), item.roleDescription(), | ||
| item.required(), item.installOrder())).toList(), | ||
| "{}", "{}", placeholderStorageKey(namespaceSlug, request.slug(), request.version()) |
| for (DraftItem item : command.items()) { | ||
| SkillBundleItemSnapshot snapshot = itemSourceResolver.resolveRegistryItem(item.skillId(), item.skillVersionId()); | ||
| SkillBundleItem entity = new SkillBundleItem( | ||
| version.getId(), BundleItemSourceType.REGISTRY, | ||
| snapshot.namespaceSlug(), snapshot.skillSlug(), snapshot.version(), | ||
| snapshot.displayName(), item.roleDescription(), | ||
| item.required(), item.installOrder()); | ||
| entity.setSkillId(item.skillId()); | ||
| entity.setSkillVersionId(item.skillVersionId()); | ||
| entity.setSummary(snapshot.summary()); | ||
| itemRepository.save(entity); | ||
| } |
| SkillBundleReviewTask task = reviewTaskRepository.findByBundleVersionId(bundleVersionId) | ||
| .orElseGet(() -> new SkillBundleReviewTask(bundleVersionId, bundle.getNamespaceId(), submitter)); | ||
| task.setStatus("PENDING"); | ||
| return reviewTaskRepository.save(task); |
背景
引入设计文档里的「技能包」聚合:把多条已发布的技能按版本快照打包成一个可分享、可安装的整体(项目类、角色类、场景类、自定义)。技能包自身有版本、有审核、有社交(收藏 / 评分 / 评论 / 下载量),并提供版本锁定的安装清单。
改动概要
数据库(Flyway V42)
skill_bundle:技能包主表(namespace、slug、type、统计字段)。skill_bundle_version:版本表,存放 manifest / lock JSON 与状态。skill_bundle_item:版本快照里的 item 行,固化技能版本 ID。skill_bundle_validation_result:构建期校验结果。skill_bundle_review_task:审核工单(DRAFT → PENDING_REVIEW → PUBLISHED | REJECTED)。skill_bundle_star/skill_bundle_rating/skill_bundle_comment/skill_bundle_download_event。skill_bundle_search_document:检索索引。Server 端
skillhub-domain/.../bundle)SkillBundle/SkillBundleVersion/SkillBundleItem/SkillBundleReviewTask及状态枚举。SkillBundleDraftService:草稿构建,强制 item 唯一、PROJECT/ROLE 类型必须有标签、按 install_order 排序、SkillBundleItemSourceResolver接口快照已发布技能版本。SkillBundleReviewService:审核状态机,含乐观锁冲突识别与自审拒绝。JpaSkillBundleItemSourceResolver:解析已发布技能元数据。SkillBundleAppService:DTO 转换 + 可见版本解析 + 下载计数。/api/v1/skill-bundles(构建草稿、详情、提交审核、下载累计)、管理端/api/v1/skill-bundle-reviews(审批驳回)。Web 端
pages/bundles/skill-bundle-detail.tsx:技能包详情、安装命令复制、技能清单链接。features/skill-bundle/api.ts+hooks.ts:envelope-aware fetch + react-query 钩子。测试
SkillBundleDraftServiceTest:6 个用例(item 重复、版本未发布、project/role 标签校验等)。SkillBundleReviewServiceTest:7 个用例(状态机迁移、自审拒绝、乐观锁、validation 状态门控)。验证
./mvnw -pl skillhub-domain -am test:BUILD SUCCESS,13/13 通过。Checklist
V42,不与已合入迁移冲突