Skip to content

feat(skill-bundle): skill bundle aggregate, draft build, review and d…#448

Open
paradoxgacsd wants to merge 1 commit into
iflytek:mainfrom
paradoxgacsd:feature/skill-bundle-management
Open

feat(skill-bundle): skill bundle aggregate, draft build, review and d…#448
paradoxgacsd wants to merge 1 commit into
iflytek:mainfrom
paradoxgacsd:feature/skill-bundle-management

Conversation

@paradoxgacsd
Copy link
Copy Markdown

背景

引入设计文档里的「技能包」聚合:把多条已发布的技能按版本快照打包成一个可分享、可安装的整体(项目类、角色类、场景类、自定义)。技能包自身有版本、有审核、有社交(收藏 / 评分 / 评论 / 下载量),并提供版本锁定的安装清单。

改动概要

数据库(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:审核状态机,含乐观锁冲突识别与自审拒绝。
  • 基础设施层:JPA 仓储 + 分页查询 + 批量状态更新。
  • 应用层
    • JpaSkillBundleItemSourceResolver:解析已发布技能元数据。
    • SkillBundleAppService:DTO 转换 + 可见版本解析 + 下载计数。
    • REST:/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 状态门控)。
  • 前端 vitest:4 个 API 客户端用例。

验证

  • JDK 21 + ./mvnw -pl skillhub-domain -am test:BUILD SUCCESS,13/13 通过。
  • 前端 vitest 4 个用例通过。

Checklist

  • 单元测试覆盖状态机与边界条件
  • Flyway 版本号沿用 V42,不与已合入迁移冲突
  • DTO / 控制器命名遵循现有 portal / admin 拆分约定

…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.
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +141 to +153
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;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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())
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

此处将 manifestJsonlockJson 硬编码为 {}。根据 PR 描述,技能包应提供“版本锁定的安装清单”,这通常依赖于 lock_json 记录各技能的具体版本。硬编码为空会导致安装清单失效,建议根据 items 列表生成真实的 JSON 内容,或者在领域层完善生成逻辑。

Comment on lines +54 to +65
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);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

在循环中逐个解析和保存技能项会导致严重的 N+1 查询问题。itemSourceResolver.resolveRegistryItemJpaSkillBundleItemSourceResolver 的实现中对每个 item 都会执行多次数据库查询。建议优化为批量解析模式,先收集所有 skillIdskillVersionId 进行一次性批量查询,以提升构建性能。

Comment on lines +47 to +50
SkillBundleReviewTask task = reviewTaskRepository.findByBundleVersionId(bundleVersionId)
.orElseGet(() -> new SkillBundleReviewTask(bundleVersionId, bundle.getNamespaceId(), submitter));
task.setStatus("PENDING");
return reviewTaskRepository.save(task);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

submitForReview 中,如果 SkillBundleReviewTask 已经存在(例如之前被驳回后重新提交),代码仅更新了状态为 PENDING,但没有更新 submittedBysubmittedAt。这会导致审核记录中的提交信息过时,无法准确反映最近一次提交的情况。建议在重新提交时同步更新这些审计字段。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant