diff --git a/mateclaw-server/src/main/java/vip/mate/workflow/service/WorkflowService.java b/mateclaw-server/src/main/java/vip/mate/workflow/service/WorkflowService.java index b0a2e9cd..2e8e1e20 100644 --- a/mateclaw-server/src/main/java/vip/mate/workflow/service/WorkflowService.java +++ b/mateclaw-server/src/main/java/vip/mate/workflow/service/WorkflowService.java @@ -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 q = new LambdaQueryWrapper() + .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() .eq(WorkflowRevisionEntity::getWorkflowId, workflowId) diff --git a/mateclaw-server/src/main/resources/messages.properties b/mateclaw-server/src/main/resources/messages.properties index e38c3353..5d8d3be2 100644 --- a/mateclaw-server/src/main/resources/messages.properties +++ b/mateclaw-server/src/main/resources/messages.properties @@ -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 diff --git a/mateclaw-server/src/main/resources/messages_en.properties b/mateclaw-server/src/main/resources/messages_en.properties index da1b0d46..61756f30 100644 --- a/mateclaw-server/src/main/resources/messages_en.properties +++ b/mateclaw-server/src/main/resources/messages_en.properties @@ -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 diff --git a/mateclaw-server/src/test/java/vip/mate/workflow/api/WorkflowControllerTest.java b/mateclaw-server/src/test/java/vip/mate/workflow/api/WorkflowControllerTest.java index 17376956..a8c7cf06 100644 --- a/mateclaw-server/src/test/java/vip/mate/workflow/api/WorkflowControllerTest.java +++ b/mateclaw-server/src/test/java/vip/mate/workflow/api/WorkflowControllerTest.java @@ -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);