fix(workspace): harden workspace create and delete

This commit is contained in:
matevip 2026-05-19 20:06:44 +08:00
parent b2f9976f44
commit 0a57cb3358
3 changed files with 38 additions and 0 deletions

View File

@ -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);
}

View File

@ -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

View File

@ -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