diff --git a/server/skillhub-app/src/main/resources/messages.properties b/server/skillhub-app/src/main/resources/messages.properties index 55aaec57..4302f540 100644 --- a/server/skillhub-app/src/main/resources/messages.properties +++ b/server/skillhub-app/src/main/resources/messages.properties @@ -93,7 +93,7 @@ error.namespace.global.members.platformAdmin.required=Only platform user adminis error.namespace.admin.required=Namespace owner or admin role required error.namespace.owner.required=Namespace owner role required error.namespace.create.platformAdminRequired=Only SKILL_ADMIN or SUPER_ADMIN can create namespaces -error.namespace.delete.hasDependencies=Namespace cannot be deleted while it still contains skills or governance records +error.namespace.delete.hasDependencies=Namespace cannot be deleted while it still contains skills, drafts or governance records error.namespace.member.owner.assignDirect=Cannot assign OWNER role directly error.namespace.member.alreadyExists=User is already a namespace member error.namespace.member.notFound=Member not found diff --git a/server/skillhub-app/src/main/resources/messages_ru.properties b/server/skillhub-app/src/main/resources/messages_ru.properties index 48fcfdb6..17927428 100644 --- a/server/skillhub-app/src/main/resources/messages_ru.properties +++ b/server/skillhub-app/src/main/resources/messages_ru.properties @@ -90,7 +90,7 @@ error.namespace.global.members.platformAdmin.required=Только админи error.namespace.admin.required=Требуется роль владельца или администратора пространства имён error.namespace.owner.required=Требуется роль владельца пространства имён error.namespace.create.platformAdminRequired=Создавать пространства имён могут только SKILL_ADMIN или SUPER_ADMIN -error.namespace.delete.hasDependencies=Пространство имён нельзя удалить, пока в нём есть скиллы или записи управления +error.namespace.delete.hasDependencies=Пространство имён нельзя удалить, пока в нём есть скиллы, черновики или записи управления error.namespace.member.owner.assignDirect=Нельзя назначить роль OWNER напрямую error.namespace.member.alreadyExists=Пользователь уже является участником пространства имён error.namespace.member.notFound=Участник не найден diff --git a/server/skillhub-app/src/main/resources/messages_zh.properties b/server/skillhub-app/src/main/resources/messages_zh.properties index a32c45fe..1e8b144e 100644 --- a/server/skillhub-app/src/main/resources/messages_zh.properties +++ b/server/skillhub-app/src/main/resources/messages_zh.properties @@ -93,7 +93,7 @@ error.namespace.global.members.platformAdmin.required=只有平台用户管理 error.namespace.admin.required=需要命名空间管理员或所有者权限 error.namespace.owner.required=需要命名空间所有者权限 error.namespace.create.platformAdminRequired=只有 SKILL_ADMIN 或 SUPER_ADMIN 可以创建命名空间 -error.namespace.delete.hasDependencies=命名空间下仍有技能或治理记录,暂时不能删除 +error.namespace.delete.hasDependencies=命名空间下仍有技能、草稿或治理记录,暂时不能删除 error.namespace.member.owner.assignDirect=不能直接分配 OWNER 角色 error.namespace.member.alreadyExists=用户已经是该命名空间成员 error.namespace.member.notFound=未找到命名空间成员 diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/authoring/AuthoringDraftFlowIntegrationTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/authoring/AuthoringDraftFlowIntegrationTest.java index 22cfeb3a..6c9cfc5c 100644 --- a/server/skillhub-app/src/test/java/com/iflytek/skillhub/authoring/AuthoringDraftFlowIntegrationTest.java +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/authoring/AuthoringDraftFlowIntegrationTest.java @@ -18,6 +18,8 @@ import com.iflytek.skillhub.domain.namespace.NamespaceMember; import com.iflytek.skillhub.domain.namespace.NamespaceMemberRepository; import com.iflytek.skillhub.domain.namespace.NamespaceRepository; import com.iflytek.skillhub.domain.namespace.NamespaceRole; +import com.iflytek.skillhub.domain.namespace.NamespaceService; +import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException; import com.iflytek.skillhub.domain.user.UserAccount; import com.iflytek.skillhub.domain.user.UserAccountRepository; import com.iflytek.skillhub.service.authoring.ValidationRunOrchestrator; @@ -93,6 +95,7 @@ class AuthoringDraftFlowIntegrationTest { @Autowired private ValidationRunService runService; @Autowired private ValidationRunOrchestrator orchestrator; @Autowired private NamespaceRepository namespaceRepository; + @Autowired private NamespaceService namespaceService; @Autowired private NamespaceMemberRepository namespaceMemberRepository; @Autowired private UserAccountRepository userAccountRepository; @@ -377,6 +380,26 @@ class AuthoringDraftFlowIntegrationTest { } } + @Test + void namespaceWithOnlyDraftRequiresDraftRemovalBeforeDeletion() { + Namespace namespace = namespaceService.createNamespace("draft-delete-team", "Draft Delete Team", null, USER); + SkillDraft draft = draftService.createDraft(namespace.getSlug(), USER, "namespace-delete-draft", null, null); + + assertThat(namespaceService.canDelete(namespace, NamespaceRole.OWNER)).isFalse(); + DomainBadRequestException conflict = assertThrows(DomainBadRequestException.class, + () -> namespaceService.deleteNamespace(namespace.getId(), USER)); + assertThat(conflict.messageCode()).isEqualTo("error.namespace.delete.hasDependencies"); + assertThat(namespaceRepository.findById(namespace.getId())).isPresent(); + assertThat(draftService.readFile(draft.getId(), USER, "SKILL.md", null).asText()) + .contains("name: namespace-delete-draft"); + + draftService.deleteDraft(draft.getId(), USER, null); + assertThat(namespaceService.canDelete(namespace, NamespaceRole.OWNER)).isTrue(); + namespaceService.deleteNamespace(namespace.getId(), USER); + assertThat(namespaceRepository.findById(namespace.getId())).isEmpty(); + assertThat(namespaceMemberRepository.findByNamespaceIdAndUserId(namespace.getId(), USER)).isEmpty(); + } + private List concurrently(int workers, Callable action) throws Exception { CountDownLatch ready = new CountDownLatch(workers); CountDownLatch start = new CountDownLatch(1); diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/authoring/SkillDraftRepository.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/authoring/SkillDraftRepository.java index 6cadaabd..db58fc03 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/authoring/SkillDraftRepository.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/authoring/SkillDraftRepository.java @@ -18,6 +18,8 @@ public interface SkillDraftRepository { List findByNamespaceId(Long namespaceId); + boolean existsByNamespaceId(Long namespaceId); + /** Case-insensitive uniqueness of (owner, name) used at creation time. */ Optional findByOwnerIdAndNameIgnoreCase(String ownerId, String name); diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/NamespaceService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/NamespaceService.java index e54379e0..9cc5f27b 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/NamespaceService.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/NamespaceService.java @@ -1,5 +1,6 @@ package com.iflytek.skillhub.domain.namespace; +import com.iflytek.skillhub.domain.authoring.SkillDraftRepository; import com.iflytek.skillhub.domain.review.PromotionRequestRepository; import com.iflytek.skillhub.domain.review.ReviewTaskRepository; import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException; @@ -20,6 +21,7 @@ public class NamespaceService { private final NamespaceMemberRepository namespaceMemberRepository; private final NamespaceAccessPolicy namespaceAccessPolicy; private final SkillRepository skillRepository; + private final SkillDraftRepository skillDraftRepository; private final ReviewTaskRepository reviewTaskRepository; private final PromotionRequestRepository promotionRequestRepository; @@ -27,12 +29,14 @@ public class NamespaceService { NamespaceMemberRepository namespaceMemberRepository, NamespaceAccessPolicy namespaceAccessPolicy, SkillRepository skillRepository, + SkillDraftRepository skillDraftRepository, ReviewTaskRepository reviewTaskRepository, PromotionRequestRepository promotionRequestRepository) { this.namespaceRepository = namespaceRepository; this.namespaceMemberRepository = namespaceMemberRepository; this.namespaceAccessPolicy = namespaceAccessPolicy; this.skillRepository = skillRepository; + this.skillDraftRepository = skillDraftRepository; this.reviewTaskRepository = reviewTaskRepository; this.promotionRequestRepository = promotionRequestRepository; } @@ -181,7 +185,8 @@ public class NamespaceService { private boolean hasDependentData(Long namespaceId) { return skillRepository.existsByNamespaceId(namespaceId) || reviewTaskRepository.existsByNamespaceId(namespaceId) - || promotionRequestRepository.existsByTargetNamespaceId(namespaceId); + || promotionRequestRepository.existsByTargetNamespaceId(namespaceId) + || skillDraftRepository.existsByNamespaceId(namespaceId); } private void assertWritable(Namespace namespace) { diff --git a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/namespace/NamespaceServiceTest.java b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/namespace/NamespaceServiceTest.java index 12c555f3..23fa9e16 100644 --- a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/namespace/NamespaceServiceTest.java +++ b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/namespace/NamespaceServiceTest.java @@ -1,5 +1,6 @@ package com.iflytek.skillhub.domain.namespace; +import com.iflytek.skillhub.domain.authoring.SkillDraftRepository; import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException; import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException; import com.iflytek.skillhub.domain.review.PromotionRequestRepository; @@ -33,6 +34,9 @@ class NamespaceServiceTest { @Mock private SkillRepository skillRepository; + @Mock + private SkillDraftRepository skillDraftRepository; + @Mock private ReviewTaskRepository reviewTaskRepository; @@ -288,6 +292,26 @@ class NamespaceServiceTest { assertFalse(namespaceService.canDelete(namespace, NamespaceRole.OWNER)); } + @Test + void draftDependencyPreventsDeleteAndDisablesDeleteCapability() { + Long namespaceId = 1L; + String owner = "owner-1"; + Namespace namespace = new Namespace("draft-team", "Draft Team", owner); + setField(namespace, "id", namespaceId); + when(namespaceRepository.findById(namespaceId)).thenReturn(Optional.of(namespace)); + when(namespaceAccessPolicy.canDelete(namespace, NamespaceRole.OWNER)).thenReturn(true); + when(namespaceMemberRepository.findByNamespaceIdAndUserId(namespaceId, owner)) + .thenReturn(Optional.of(new NamespaceMember(namespaceId, owner, NamespaceRole.OWNER))); + when(skillDraftRepository.existsByNamespaceId(namespaceId)).thenReturn(true); + + assertFalse(namespaceService.canDelete(namespace, NamespaceRole.OWNER)); + DomainBadRequestException exception = assertThrows(DomainBadRequestException.class, + () -> namespaceService.deleteNamespace(namespaceId, owner)); + assertEquals("error.namespace.delete.hasDependencies", exception.messageCode()); + verify(namespaceMemberRepository, never()).deleteByNamespaceId(namespaceId); + verify(namespaceRepository, never()).delete(any()); + } + private void setField(Object target, String fieldName, Object value) { try { Field field = target.getClass().getDeclaredField(fieldName); diff --git a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/SkillDraftJpaRepository.java b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/SkillDraftJpaRepository.java index bb67d8e2..1eebdd5b 100644 --- a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/SkillDraftJpaRepository.java +++ b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/SkillDraftJpaRepository.java @@ -23,6 +23,8 @@ public interface SkillDraftJpaRepository extends JpaRepository List findByNamespaceId(Long namespaceId); + boolean existsByNamespaceId(Long namespaceId); + Optional findByOwnerIdAndNameIgnoreCase(String ownerId, String name); boolean existsByIdAndOwnerId(Long id, String ownerId);