fix(workflow): pre-check unique name on create/rename, return 409 instead of 500 (Fixes Gitee #IJPYWA)

This commit is contained in:
matevip 2026-05-26 23:35:04 +08:00
parent b64a312994
commit ac090afde3
4 changed files with 59 additions and 1 deletions

View File

@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import vip.mate.exception.MateClawException;
import vip.mate.workflow.compiler.PublishContext;
import vip.mate.workflow.compiler.WorkflowAclPort;
import vip.mate.workflow.compiler.WorkflowCompiler;
@ -112,6 +113,10 @@ public class WorkflowService {
@Transactional
public WorkflowEntity create(WorkflowEntity workflow) {
if (workflow.getEnabled() == null) workflow.setEnabled(true);
// Pre-check name uniqueness so the duplicate path surfaces as a friendly
// 409 instead of an opaque 500 from the uk_workflow_workspace_name
// unique index hitting the catch-all handler.
requireUniqueName(workflow.getWorkspaceId(), workflow.getName(), null);
workflowMapper.insert(workflow);
return workflow;
}
@ -128,7 +133,10 @@ public class WorkflowService {
public WorkflowEntity updateMetadata(long id, long workspaceId, String name,
String description, Boolean enabled) {
WorkflowEntity existing = getOrThrow(id, workspaceId);
if (name != null) existing.setName(name);
if (name != null && !name.equals(existing.getName())) {
requireUniqueName(workspaceId, name, id);
existing.setName(name);
}
if (description != null) existing.setDescription(description);
if (enabled != null) existing.setEnabled(enabled);
// draftJson / latest_revision_id / workspace_id are intentionally
@ -216,6 +224,35 @@ public class WorkflowService {
return new PublishOutcome(workflow, revision);
}
/**
* Reject a create / rename whose name already exists in the workspace. The
* underlying table carries a unique index on (workspace_id, name, deleted),
* so without this pre-check the duplicate insert would bubble up as a
* generic 500 with a database-level "duplicate entry" message. Pass a
* non-null {@code excludeId} on rename so the row doesn't see itself as a
* conflict.
*/
private void requireUniqueName(Long workspaceId, String name, Long excludeId) {
if (name == null || name.isBlank()) {
throw new MateClawException("err.workflow.name_required", 400,
"工作流名称不能为空");
}
if (workspaceId == null) {
return;
}
LambdaQueryWrapper<WorkflowEntity> q = new LambdaQueryWrapper<WorkflowEntity>()
.eq(WorkflowEntity::getWorkspaceId, workspaceId)
.eq(WorkflowEntity::getName, name);
if (excludeId != null) {
q.ne(WorkflowEntity::getId, excludeId);
}
Long count = workflowMapper.selectCount(q);
if (count != null && count > 0) {
throw new MateClawException("err.workflow.duplicate_name", 409,
"工作区内已存在同名工作流: " + name);
}
}
private int nextRevisionNumber(long workflowId) {
WorkflowRevisionEntity max = revisionMapper.selectOne(new LambdaQueryWrapper<WorkflowRevisionEntity>()
.eq(WorkflowRevisionEntity::getWorkflowId, workflowId)

View File

@ -171,6 +171,8 @@ err.agent.not_found=Agent\u4e0d\u5b58\u5728
err.agent.disabled=Agent \u5df2\u7981\u7528
err.agent.name_required=Agent \u540d\u79f0\u4e0d\u80fd\u4e3a\u7a7a
err.agent.duplicate_name=\u5f53\u524d\u5de5\u4f5c\u533a\u5df2\u5b58\u5728\u540c\u540d\u5458\u5de5\uff0c\u8bf7\u6362\u4e2a\u540d\u5b57\u518d\u8bd5
err.workflow.name_required=\u5de5\u4f5c\u6d41\u540d\u79f0\u4e0d\u80fd\u4e3a\u7a7a
err.workflow.duplicate_name=\u5f53\u524d\u5de5\u4f5c\u533a\u5df2\u5b58\u5728\u540c\u540d\u5de5\u4f5c\u6d41\uff0c\u8bf7\u6362\u4e2a\u540d\u5b57\u518d\u8bd5
err.workspace.not_found=\u5de5\u4f5c\u533a\u4e0d\u5b58\u5728
err.workspace.slug_exists=\u5de5\u4f5c\u533a\u6807\u8bc6\u5df2\u5b58\u5728
err.workspace.cannot_modify_default=\u4e0d\u80fd\u4fee\u6539\u9ed8\u8ba4\u5de5\u4f5c\u533a\u7684\u6807\u8bc6

View File

@ -177,6 +177,8 @@ err.agent.not_found=Agent not found
err.agent.disabled=Agent is disabled
err.agent.name_required=Agent name is required
err.agent.duplicate_name=An employee with this name already exists in this workspace — try a different name
err.workflow.name_required=Workflow name is required
err.workflow.duplicate_name=A workflow with this name already exists in this workspace — try a different name
# workspace
err.workspace.not_found=Workspace not found
err.workspace.slug_exists=Workspace slug already exists

View File

@ -13,12 +13,14 @@ import org.springframework.http.ResponseEntity;
import org.springframework.test.context.TestPropertySource;
import vip.mate.MateClawApplication;
import vip.mate.common.result.R;
import vip.mate.exception.MateClawException;
import vip.mate.workflow.compiler.WorkflowAclPort;
import vip.mate.workflow.model.WorkflowEntity;
import vip.mate.workflow.repository.WorkflowMapper;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
@ -97,6 +99,21 @@ class WorkflowControllerTest {
assertInstanceOf(CompileErrorResponse.class, body.getData());
}
@Test
@DisplayName("create() rejects a second workflow with the same name as a 409 instead of a 500 from the unique index.")
void createRejectsDuplicateNameAs409() {
WorkflowEntity first = new WorkflowEntity();
first.setName("dup");
controller.create(first, 99L);
WorkflowEntity second = new WorkflowEntity();
second.setName("dup");
MateClawException ex = assertThrows(MateClawException.class,
() -> controller.create(second, 99L));
assertEquals(409, ex.getCode());
assertEquals("err.workflow.duplicate_name", ex.getMsgKey());
}
private Long createWorkflow(String name) {
WorkflowEntity wf = new WorkflowEntity();
wf.setWorkspaceId(99L);