diff --git a/algorithm-service/algorithm-ai-service/src/main/java/com/stephen/cloud/ai/controller/KnowledgeBaseController.java b/algorithm-service/algorithm-ai-service/src/main/java/com/stephen/cloud/ai/controller/KnowledgeBaseController.java index c11c3a0..9ec4cc1 100644 --- a/algorithm-service/algorithm-ai-service/src/main/java/com/stephen/cloud/ai/controller/KnowledgeBaseController.java +++ b/algorithm-service/algorithm-ai-service/src/main/java/com/stephen/cloud/ai/controller/KnowledgeBaseController.java @@ -19,6 +19,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletRequest; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.web.bind.annotation.*; /** @@ -48,7 +49,12 @@ public BaseResponse addKnowledgeBase(@RequestBody KnowledgeBaseAddRequest ThrowUtils.throwIf(!knowledgeBaseService.isNameUnique(knowledgeBase.getName(), null), ErrorCode.OPERATION_ERROR, "知识库名称已存在"); knowledgeBase.setUserId(SecurityUtils.getLoginUserId()); knowledgeBase.setDocumentCount(0); - boolean result = knowledgeBaseService.save(knowledgeBase); + boolean result; + try { + result = knowledgeBaseService.save(knowledgeBase); + } catch (DataIntegrityViolationException e) { + throw new BusinessException(ErrorCode.OPERATION_ERROR, "知识库名称已存在"); + } ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR); return ResultUtils.success(knowledgeBase.getId()); } diff --git a/algorithm-service/algorithm-ai-service/src/test/java/com/stephen/cloud/ai/controller/KnowledgeBaseControllerTest.java b/algorithm-service/algorithm-ai-service/src/test/java/com/stephen/cloud/ai/controller/KnowledgeBaseControllerTest.java new file mode 100644 index 0000000..03dface --- /dev/null +++ b/algorithm-service/algorithm-ai-service/src/test/java/com/stephen/cloud/ai/controller/KnowledgeBaseControllerTest.java @@ -0,0 +1,57 @@ +package com.stephen.cloud.ai.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.stephen.cloud.ai.service.KnowledgeBaseService; +import com.stephen.cloud.api.ai.model.dto.knowledgebase.KnowledgeBaseAddRequest; +import com.stephen.cloud.common.auth.utils.SecurityUtils; +import com.stephen.cloud.common.common.ErrorCode; +import com.stephen.cloud.common.web.exception.GlobalExceptionHandler; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.MediaType; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class KnowledgeBaseControllerTest { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void addKnowledgeBaseShouldReturnFriendlyMessageWhenUniqueConstraintIsHit() throws Exception { + KnowledgeBaseService knowledgeBaseService = mock(KnowledgeBaseService.class); + when(knowledgeBaseService.isNameUnique("排序算法知识库", null)).thenReturn(true); + when(knowledgeBaseService.save(any())) + .thenThrow(new DataIntegrityViolationException("Duplicate entry")); + + KnowledgeBaseController controller = new KnowledgeBaseController(); + ReflectionTestUtils.setField(controller, "knowledgeBaseService", knowledgeBaseService); + + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller) + .setControllerAdvice(new GlobalExceptionHandler()) + .build(); + + KnowledgeBaseAddRequest request = new KnowledgeBaseAddRequest(); + request.setName("排序算法知识库"); + request.setDescription("排序算法知识库"); + + try (MockedStatic securityUtils = org.mockito.Mockito.mockStatic(SecurityUtils.class)) { + securityUtils.when(SecurityUtils::getLoginUserId).thenReturn(1L); + + mockMvc.perform(post("/ai/kb/add") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(ErrorCode.OPERATION_ERROR.getCode())) + .andExpect(jsonPath("$.message").value("知识库名称已存在")); + } + } +} diff --git a/sql/README.md b/sql/README.md index 6bc50ef..1cced16 100644 --- a/sql/README.md +++ b/sql/README.md @@ -29,6 +29,13 @@ done mysql -u root -p < ai_chat_record.sql ``` +### 3. 保持表结构为最新版本 + +`algorithm.sql` 已包含最新的 `knowledge_base` 表结构,其中知识库唯一索引已更新为 `(name, is_delete)`,可支持逻辑删除后重建同名知识库。 + +如果是新环境,直接执行最新版 `algorithm.sql` 即可。 +如果是已有环境,请将现网 `knowledge_base` 表的唯一索引同步到 `algorithm.sql` 中的最新定义后,再发布包含后端异常兜底的服务版本。 + ## 📝 表设计规范 - **字符集**: 统一使用 `utf8mb4`,支持 Emoji 存储。 @@ -44,3 +51,4 @@ mysql -u root -p < ai_chat_record.sql 1. **环境差异**: 生产环境建议通过 Nacos 或环境变量动态注入数据库连接信息。 2. **性能优化**: 日志类表(如 `api_access_log`)建议定期归档或清理。 3. **安全**: 避免修改 `root` 权限直接运行,建议为每个库创建独立账号。 +4. **知识库重名修复**: 若知识库采用逻辑删除,唯一索引需要包含 `is_delete`,否则“删除后重建同名知识库”会在数据库层失败。 diff --git a/sql/algorithm.sql b/sql/algorithm.sql index e36059d..ba41eb9 100644 --- a/sql/algorithm.sql +++ b/sql/algorithm.sql @@ -159,7 +159,7 @@ CREATE TABLE `knowledge_base` `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `is_delete` tinyint NOT NULL DEFAULT 0 COMMENT '是否删除', PRIMARY KEY (`id`), - UNIQUE KEY `uk_name` (`name`) COMMENT '知识库名称唯一索引', + UNIQUE KEY `uk_name_is_delete` (`name`, `is_delete`) COMMENT '知识库名称+删除状态唯一索引', KEY `idx_user_id` (`user_id`) COMMENT '创建用户ID索引' ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4