From 6b2d8ce7ef17904f317538576a90ac0799cf565f Mon Sep 17 00:00:00 2001 From: yun-zhi-ztl <66589705+yun-zhi-ztl@users.noreply.github.com> Date: Wed, 18 Mar 2026 16:40:51 +0800 Subject: [PATCH] fix: skill upload constraints, rejected label, and last-version guard (#82) * fix: keep download counts consistent across skill pages * fix: stabilize empty search ordering across sorts * fix: show disabled-account reason on login redirect * fix: mute report input placeholder text * fix: return skill detail to my skills page * test: stabilize auth context filter coverage * feat(publish): increase single file limit to 10MB * feat(publish): expand allowed file extensions * feat(publish): extend secret scanning to new text file types * feat(publish): add content validation for new file types * refactor(publish): inject configurable limits into SkillPackageArchiveExtractor * feat(publish): support zip with single root directory wrapper * feat(publish): expand determineContentType for new file types * test(publish): update tests for new upload constraints * fix(web): add REJECTED status label and styling to my-skills page The resolveStatusLabel and resolveStatusClassName functions were missing the REJECTED case, causing rejected skills to show raw status string with no color styling. * fix: prevent deleting the last remaining version of a skill Backend: added version count check in SkillGovernanceService.deleteVersion() Frontend: hide delete button when only one version remains * test: fix and add tests for last-version deletion guard - Fix deleteVersion_removesDraftFilesAndBundle: mock findBySkillId to return 2 versions so the new guard doesn't block the happy path - Add deleteVersion_rejectsLastRemainingVersion: verify that deleting the only remaining version is rejected with the correct error code --------- --- .../src/main/resources/messages.properties | 1 + .../src/main/resources/messages_zh.properties | 1 + .../skill/service/SkillGovernanceService.java | 5 +++++ .../service/SkillGovernanceServiceTest.java | 19 +++++++++++++++++++ web/src/i18n/locales/en.json | 1 + web/src/i18n/locales/zh.json | 1 + web/src/index.css | 1 + web/src/pages/dashboard/my-skills.tsx | 6 ++++++ web/src/pages/skill-detail.tsx | 3 ++- 9 files changed, 37 insertions(+), 1 deletion(-) diff --git a/server/skillhub-app/src/main/resources/messages.properties b/server/skillhub-app/src/main/resources/messages.properties index f9ced2ca..6d099224 100644 --- a/server/skillhub-app/src/main/resources/messages.properties +++ b/server/skillhub-app/src/main/resources/messages.properties @@ -94,6 +94,7 @@ error.skill.version.exists=Version already exists: {0} error.skill.version.notFound=Version not found: {0} error.skill.version.notPublished=Version is not published: {0} error.skill.version.delete.unsupported=Only DRAFT or REJECTED versions can be deleted: {0} +error.skill.version.delete.lastVersion=Cannot delete the last remaining version: {0} error.skill.report.reason.required=Please provide a report reason error.skill.report.unavailable=This skill cannot be reported right now: {0} error.skill.report.self=You cannot report your own skill diff --git a/server/skillhub-app/src/main/resources/messages_zh.properties b/server/skillhub-app/src/main/resources/messages_zh.properties index da8df0b8..b98d4ad5 100644 --- a/server/skillhub-app/src/main/resources/messages_zh.properties +++ b/server/skillhub-app/src/main/resources/messages_zh.properties @@ -94,6 +94,7 @@ error.skill.version.exists=版本已存在:{0} error.skill.version.notFound=未找到版本:{0} error.skill.version.notPublished=版本未发布:{0} error.skill.version.delete.unsupported=只有 DRAFT 或 REJECTED 版本可以删除:{0} +error.skill.version.delete.lastVersion=无法删除最后一个版本:{0} error.skill.report.reason.required=请填写举报原因 error.skill.report.unavailable=当前无法举报该技能:{0} error.skill.report.self=不能举报自己发布的技能 diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceService.java index 9588c6d4..edce6d81 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceService.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceService.java @@ -141,6 +141,11 @@ public class SkillGovernanceService { throw new DomainBadRequestException("error.skill.version.delete.unsupported", version.getVersion()); } + long versionCount = skillVersionRepository.findBySkillId(skill.getId()).size(); + if (versionCount <= 1) { + throw new DomainBadRequestException("error.skill.version.delete.lastVersion", version.getVersion()); + } + List files = skillFileRepository.findByVersionId(version.getId()); if (!files.isEmpty()) { objectStorageService.deleteObjects(files.stream().map(SkillFile::getStorageKey).toList()); diff --git a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceServiceTest.java b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceServiceTest.java index e9923e14..ea88df40 100644 --- a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceServiceTest.java +++ b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceServiceTest.java @@ -181,6 +181,10 @@ class SkillGovernanceServiceTest { SkillVersion version = new SkillVersion(2L, "1.0.0", "owner"); setField(version, "id", 2L); version.setStatus(SkillVersionStatus.DRAFT); + SkillVersion otherVersion = new SkillVersion(2L, "2.0.0", "owner"); + setField(otherVersion, "id", 3L); + otherVersion.setStatus(SkillVersionStatus.PUBLISHED); + given(skillVersionRepository.findBySkillId(1L)).willReturn(java.util.List.of(version, otherVersion)); SkillFile readme = new SkillFile(version.getId(), "README.md", 10L, "text/markdown", "sha1", "skills/demo/readme"); SkillFile icon = new SkillFile(version.getId(), "icon.png", 20L, "image/png", "sha2", "skills/demo/icon"); given(skillFileRepository.findByVersionId(version.getId())).willReturn(java.util.List.of(readme, icon)); @@ -212,6 +216,21 @@ class SkillGovernanceServiceTest { verify(objectStorageService, never()).deleteObject(any()); } + @Test + void deleteVersion_rejectsLastRemainingVersion() { + Skill skill = new Skill(1L, "demo", "owner", com.iflytek.skillhub.domain.skill.SkillVisibility.PUBLIC); + setField(skill, "id", 1L); + SkillVersion version = new SkillVersion(2L, "1.0.0", "owner"); + setField(version, "id", 2L); + version.setStatus(SkillVersionStatus.DRAFT); + given(skillVersionRepository.findBySkillId(1L)).willReturn(java.util.List.of(version)); + + DomainBadRequestException ex = assertThrows(DomainBadRequestException.class, + () -> service.deleteVersion(skill, version, "owner", Map.of(), "127.0.0.1", "JUnit")); + assertThat(ex.messageCode()).isEqualTo("error.skill.version.delete.lastVersion"); + + verify(skillVersionRepository, never()).delete(any()); + } private void setField(Object target, String fieldName, Object value) { try { java.lang.reflect.Field field = target.getClass().getDeclaredField(fieldName); diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index f1089eab..e2bf72d1 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -289,6 +289,7 @@ "statusArchived": "Archived", "statusPendingReview": "Pending Review", "statusPublished": "Published", + "statusRejected": "Rejected", "archiveConfirmTitle": "Archive skill", "archiveConfirmDescription": "After archiving, regular users will no longer be able to view or download \"{{skill}}\". Continue?", "unarchiveConfirmTitle": "Restore skill", diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index cf2c16d6..95fa8998 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -289,6 +289,7 @@ "statusArchived": "已归档", "statusPendingReview": "审核中", "statusPublished": "已发布", + "statusRejected": "已拒绝", "archiveConfirmTitle": "确认归档技能", "archiveConfirmDescription": "归档后普通用户将无法看到或下载“{{skill}}”,确定继续吗?", "unarchiveConfirmTitle": "确认恢复技能", diff --git a/web/src/index.css b/web/src/index.css index cec718cb..a46c59a9 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -344,6 +344,7 @@ .status-pill--published { background: #16a34a; } .status-pill--review { background: #ea580c; } .status-pill--archived { background: #6b7280; } +.status-pill--rejected { background: #dc2626; } /* ─── Role pill ─── */ .role-pill { diff --git a/web/src/pages/dashboard/my-skills.tsx b/web/src/pages/dashboard/my-skills.tsx index c489feaa..da406ba6 100644 --- a/web/src/pages/dashboard/my-skills.tsx +++ b/web/src/pages/dashboard/my-skills.tsx @@ -58,6 +58,9 @@ export function MySkillsPage() { if (status === 'PUBLISHED') { return t('mySkills.statusPublished') } + if (status === 'REJECTED') { + return t('mySkills.statusRejected') + } return status } @@ -71,6 +74,9 @@ export function MySkillsPage() { if (status === 'PUBLISHED') { return 'status-pill status-pill--published' } + if (status === 'REJECTED') { + return 'status-pill status-pill--rejected' + } return 'status-pill' } diff --git a/web/src/pages/skill-detail.tsx b/web/src/pages/skill-detail.tsx index 67839e39..c51e9630 100644 --- a/web/src/pages/skill-detail.tsx +++ b/web/src/pages/skill-detail.tsx @@ -307,6 +307,7 @@ export function SkillDetailPage() { } const canDeleteVersion = (status?: string) => status === 'DRAFT' || status === 'REJECTED' + const isLastVersion = versions?.length === 1 const canWithdrawVersion = (status?: string) => status === 'PENDING_REVIEW' const canRereleaseVersion = (status?: string) => status === 'PUBLISHED' @@ -687,7 +688,7 @@ export function SkillDetailPage() { )} - {skill.canManageLifecycle && canDeleteVersion(version.status) && ( + {skill.canManageLifecycle && canDeleteVersion(version.status) && !isLastVersion && (