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 void deleteVersion(Skill skill, 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 @@ void deleteVersion_removesDraftFilesAndBundle() { 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 @@ void deleteVersion_rejectsPublishedVersion() { 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 83645f49..936ecf8c 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 f4b9e035..e74cf224 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 94936b61..ce40fdd0 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 05abbc66..1f4bb38f 100644 --- a/web/src/pages/skill-detail.tsx +++ b/web/src/pages/skill-detail.tsx @@ -240,6 +240,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' @@ -590,7 +591,7 @@ export function SkillDetailPage() { )} - {skill.canManageLifecycle && canDeleteVersion(version.status) && ( + {skill.canManageLifecycle && canDeleteVersion(version.status) && !isLastVersion && (