From 0a57cb33585a6c50e032bdaa1ad2b7dee201fd03 Mon Sep 17 00:00:00 2001 From: matevip Date: Tue, 19 May 2026 20:06:44 +0800 Subject: [PATCH] fix(workspace): harden workspace create and delete --- .../core/service/WorkspaceService.java | 36 +++++++++++++++++++ .../src/main/resources/messages.properties | 1 + .../src/main/resources/messages_en.properties | 1 + 3 files changed, 38 insertions(+) diff --git a/mateclaw-server/src/main/java/vip/mate/workspace/core/service/WorkspaceService.java b/mateclaw-server/src/main/java/vip/mate/workspace/core/service/WorkspaceService.java index 69182bae..dce984b3 100644 --- a/mateclaw-server/src/main/java/vip/mate/workspace/core/service/WorkspaceService.java +++ b/mateclaw-server/src/main/java/vip/mate/workspace/core/service/WorkspaceService.java @@ -9,6 +9,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import vip.mate.exception.MateClawException; import vip.mate.i18n.I18nService; +import vip.mate.wiki.service.WikiKnowledgeBaseService; import vip.mate.workspace.conversation.model.ConversationEntity; import vip.mate.workspace.conversation.repository.ConversationMapper; import vip.mate.workspace.core.model.WorkspaceAccessVO; @@ -39,6 +40,7 @@ public class WorkspaceService { private final WorkspaceMapper workspaceMapper; private final WorkspaceMemberMapper memberMapper; private final ConversationMapper conversationMapper; + private final WikiKnowledgeBaseService wikiKnowledgeBaseService; private final I18nService i18n; /** 默认工作区 slug */ @@ -134,8 +136,34 @@ public class WorkspaceService { .eq(WorkspaceEntity::getSlug, slug)); } + /** + * Derive a URL-safe, unique slug from a workspace name. Non-alphanumeric + * runs collapse to a single hyphen; a name with no ASCII alphanumerics + * (e.g. a purely Chinese name) falls back to a generic base. A numeric + * suffix is appended until the slug is free. + */ + private String generateUniqueSlug(String name) { + String base = (name == null ? "" : name.toLowerCase()) + .replaceAll("[^a-z0-9]+", "-") + .replaceAll("^-|-$", ""); + if (base.isBlank()) { + base = "workspace"; + } + String slug = base; + int n = 1; + while (getBySlug(slug) != null) { + slug = base + "-" + (++n); + } + return slug; + } + @Transactional public WorkspaceEntity create(WorkspaceEntity entity, Long creatorUserId) { + // Auto-derive a slug from the name when the caller did not supply one, + // so workspace creation never fails on the NOT NULL slug column. + if (entity.getSlug() == null || entity.getSlug().isBlank()) { + entity.setSlug(generateUniqueSlug(entity.getName())); + } // 验证 slug 唯一 if (getBySlug(entity.getSlug()) != null) { throw new MateClawException("err.workspace.slug_exists", "工作区标识已存在: " + entity.getSlug()); @@ -206,6 +234,14 @@ public class WorkspaceService { if (DEFAULT_SLUG.equals(existing.getSlug())) { throw new MateClawException("err.workspace.cannot_delete_default", "不能删除默认工作区"); } + // Refuse to delete a workspace that still owns wiki knowledge bases — + // dropping the workspace row would orphan them. The caller must delete + // the knowledge bases first. + int kbCount = wikiKnowledgeBaseService.listByWorkspace(id).size(); + if (kbCount > 0) { + throw new MateClawException("err.workspace.not_empty", 409, + "工作区下还有 " + kbCount + " 个知识库,请先删除知识库再删除工作区"); + } workspaceMapper.deleteById(id); log.info("Deleted workspace: {} (id={})", existing.getName(), id); } diff --git a/mateclaw-server/src/main/resources/messages.properties b/mateclaw-server/src/main/resources/messages.properties index 64b23c2d..6896a568 100644 --- a/mateclaw-server/src/main/resources/messages.properties +++ b/mateclaw-server/src/main/resources/messages.properties @@ -173,6 +173,7 @@ err.workspace.not_member=\u7528\u6237\u4e0d\u662f\u8be5\u5de5\u4f5c\u533a\u7684\ err.workspace.cannot_modify_owner=\u4e0d\u80fd\u4fee\u6539\u5de5\u4f5c\u533a\u62e5\u6709\u8005\u7684\u89d2\u8272 err.workspace.cannot_remove_owner=\u4e0d\u80fd\u79fb\u9664\u5de5\u4f5c\u533a\u62e5\u6709\u8005 err.workspace.insufficient_permission=\u6743\u9650\u4e0d\u8db3 +err.workspace.not_empty=\u5de5\u4f5c\u533a\u4e0b\u8fd8\u6709\u77e5\u8bc6\u5e93\uff0c\u8bf7\u5148\u5220\u9664\u77e5\u8bc6\u5e93\u540e\u518d\u5220\u9664\u5de5\u4f5c\u533a err.channel.not_found=\u6e20\u9053\u4e0d\u5b58\u5728 err.channel.name_required=\u6e20\u9053\u540d\u79f0\u4e0d\u80fd\u4e3a\u7a7a err.channel.type_required=\u6e20\u9053\u7c7b\u578b\u4e0d\u80fd\u4e3a\u7a7a diff --git a/mateclaw-server/src/main/resources/messages_en.properties b/mateclaw-server/src/main/resources/messages_en.properties index 9454cfc8..a792c4a0 100644 --- a/mateclaw-server/src/main/resources/messages_en.properties +++ b/mateclaw-server/src/main/resources/messages_en.properties @@ -180,6 +180,7 @@ err.workspace.not_member=User is not a member of this workspace err.workspace.cannot_modify_owner=Cannot modify workspace owner role err.workspace.cannot_remove_owner=Cannot remove workspace owner err.workspace.insufficient_permission=Insufficient permission +err.workspace.not_empty=Workspace still contains knowledge bases; delete them first # channel err.channel.not_found=Channel not found err.channel.name_required=Channel name cannot be empty