diff --git a/docs/script/sql/ruoyi-ai.sql b/docs/script/sql/ruoyi-ai.sql index 99c2f027..1a874e81 100644 --- a/docs/script/sql/ruoyi-ai.sql +++ b/docs/script/sql/ruoyi-ai.sql @@ -1131,7 +1131,8 @@ CREATE TABLE `knowledge_attach` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `knowledge_id` bigint NOT NULL COMMENT '知识库ID', `oss_id` bigint NULL DEFAULT NULL COMMENT '对象存储ID', - `doc_id` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '文档ID', + `doc_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '文档ID', + `file_hash` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '文件SHA-256摘要', `name` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '附件名称', `type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '附件类型', `create_dept` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '部门', @@ -1143,7 +1144,8 @@ CREATE TABLE `knowledge_attach` ( `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户Id', `status` tinyint NULL DEFAULT 0 COMMENT '解析状态: 0待解析, 1解析中, 2已解析, 3解析失败', PRIMARY KEY (`id`) USING BTREE, - UNIQUE INDEX `idx_kname`(`knowledge_id` ASC, `name` ASC) USING BTREE + UNIQUE INDEX `idx_kname`(`knowledge_id` ASC, `name` ASC) USING BTREE, + UNIQUE INDEX `uk_knowledge_file_hash`(`knowledge_id`, `file_hash`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 2033199209203183619 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '知识库附件' ROW_FORMAT = DYNAMIC; -- ---------------------------- @@ -1156,8 +1158,9 @@ CREATE TABLE `knowledge_attach` ( DROP TABLE IF EXISTS `knowledge_fragment`; CREATE TABLE `knowledge_fragment` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', + `fid` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '向量库片段ID', `idx` int NOT NULL COMMENT '片段索引下标', - `doc_id` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '文档ID', + `doc_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '文档ID', `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '文档内容', `create_dept` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '部门', `create_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '创建人', @@ -1168,6 +1171,9 @@ CREATE TABLE `knowledge_fragment` ( `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户Id', `knowledge_id` bigint NULL DEFAULT NULL COMMENT '知识库ID', PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_fid`(`fid`) USING BTREE, + INDEX `idx_doc_id`(`doc_id`) USING BTREE, + INDEX `idx_knowledge_id`(`knowledge_id`) USING BTREE, FULLTEXT INDEX `ft_content`(`content`) WITH PARSER `ngram` ) ENGINE = InnoDB AUTO_INCREMENT = 2033199209131880451 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '知识片段' ROW_FORMAT = DYNAMIC; @@ -1206,7 +1212,9 @@ CREATE TABLE `knowledge_info` ( `enable_hybrid` tinyint(1) NULL DEFAULT 0 COMMENT '是否启用混合检索', `hybrid_alpha` double NULL DEFAULT 0.5 COMMENT '混合检索权重比例 (0.0=纯向量, 1.0=纯关键词)', `system_prompt` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '系统提示词', - PRIMARY KEY (`id`) USING BTREE + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_tenant_user` (`tenant_id`, `user_id`) USING BTREE, + INDEX `idx_tenant_share` (`tenant_id`, `share`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 2033198818050781187 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '知识库' ROW_FORMAT = DYNAMIC; -- ---------------------------- diff --git a/docs/script/sql/update/2026-07-20-knowledge-fragment-fid.sql b/docs/script/sql/update/2026-07-20-knowledge-fragment-fid.sql new file mode 100644 index 00000000..58b83adb --- /dev/null +++ b/docs/script/sql/update/2026-07-20-knowledge-fragment-fid.sql @@ -0,0 +1,44 @@ +-- RAG metadata migration (MySQL 8). Safe to execute repeatedly. +ALTER TABLE `knowledge_attach` + ADD COLUMN IF NOT EXISTS `file_hash` varchar(64) NULL DEFAULT NULL COMMENT '文件SHA-256摘要' AFTER `doc_id`, + MODIFY COLUMN `doc_id` varchar(32) NULL DEFAULT NULL COMMENT '文档ID'; + +SET @add_file_hash = IF(EXISTS( + SELECT 1 FROM information_schema.statistics WHERE table_schema = DATABASE() + AND table_name = 'knowledge_attach' AND index_name = 'uk_knowledge_file_hash'), + 'SELECT 1', 'ALTER TABLE `knowledge_attach` ADD UNIQUE INDEX `uk_knowledge_file_hash` (`knowledge_id`, `file_hash`)'); +PREPARE stmt FROM @add_file_hash; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +ALTER TABLE `knowledge_fragment` + ADD COLUMN IF NOT EXISTS `fid` varchar(32) NULL DEFAULT NULL COMMENT '向量库片段ID' AFTER `id`, + MODIFY COLUMN `doc_id` varchar(32) NULL DEFAULT NULL COMMENT '文档ID'; + +UPDATE `knowledge_fragment` +SET `fid` = LOWER(MD5(CONCAT('knowledge_fragment:', `id`))) +WHERE `fid` IS NULL OR `fid` = ''; + +SET @drop_idx_fid = IF(EXISTS( + SELECT 1 FROM information_schema.statistics WHERE table_schema = DATABASE() + AND table_name = 'knowledge_fragment' AND index_name = 'idx_fid'), + 'ALTER TABLE `knowledge_fragment` DROP INDEX `idx_fid`', 'SELECT 1'); +PREPARE stmt FROM @drop_idx_fid; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @add_uk_fid = IF(EXISTS( + SELECT 1 FROM information_schema.statistics WHERE table_schema = DATABASE() + AND table_name = 'knowledge_fragment' AND index_name = 'uk_fid'), + 'SELECT 1', 'ALTER TABLE `knowledge_fragment` ADD UNIQUE INDEX `uk_fid` (`fid`)'); +PREPARE stmt FROM @add_uk_fid; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +ALTER TABLE `knowledge_fragment` MODIFY COLUMN `fid` varchar(32) NOT NULL COMMENT '向量库片段ID'; + +SET @add_tenant_user = IF(EXISTS( + SELECT 1 FROM information_schema.statistics WHERE table_schema = DATABASE() + AND table_name = 'knowledge_info' AND index_name = 'idx_tenant_user'), + 'SELECT 1', 'ALTER TABLE `knowledge_info` ADD INDEX `idx_tenant_user` (`tenant_id`, `user_id`)'); +PREPARE stmt FROM @add_tenant_user; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @add_tenant_share = IF(EXISTS( + SELECT 1 FROM information_schema.statistics WHERE table_schema = DATABASE() + AND table_name = 'knowledge_info' AND index_name = 'idx_tenant_share'), + 'SELECT 1', 'ALTER TABLE `knowledge_info` ADD INDEX `idx_tenant_share` (`tenant_id`, `share`)'); +PREPARE stmt FROM @add_tenant_share; EXECUTE stmt; DEALLOCATE PREPARE stmt; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/knowledge/KnowledgeAttachController.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/knowledge/KnowledgeAttachController.java index e2e1b4e8..b5bab948 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/knowledge/KnowledgeAttachController.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/knowledge/KnowledgeAttachController.java @@ -106,7 +106,10 @@ public class KnowledgeAttachController extends BaseController { /** * 上传知识库附件 + * 注意:multipart 上传不能加 @RepeatSubmit(其参数序列化不支持 MultipartFile) */ + @SaCheckPermission("system:attach:add") + @Log(title = "知识库附件", businessType = BusinessType.INSERT) @PostMapping(value = "/upload") public R upload(KnowledgeInfoUploadBo bo){ knowledgeAttachService.upload(bo); @@ -118,7 +121,10 @@ public class KnowledgeAttachController extends BaseController { * * @param id 附件ID */ + @SaCheckPermission("system:attach:edit") + @Log(title = "知识库附件", businessType = BusinessType.UPDATE) @PostMapping("/parse/{id}") + @RepeatSubmit() public R parse(@PathVariable Long id) { knowledgeAttachService.parse(id); return R.ok(); diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/knowledge/KnowledgeFragmentController.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/knowledge/KnowledgeFragmentController.java index d739a30f..c3a5a13e 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/knowledge/KnowledgeFragmentController.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/knowledge/KnowledgeFragmentController.java @@ -107,7 +107,9 @@ public class KnowledgeFragmentController extends BaseController { /** * 检索测试 */ + @SaCheckPermission("system:fragment:list") @PostMapping("/retrieval") + @RepeatSubmit() public R> retrieval(@RequestBody KnowledgeFragmentBo bo) { return R.ok(knowledgeFragmentService.retrieval(bo)); } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/entity/knowledge/KnowledgeAttach.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/entity/knowledge/KnowledgeAttach.java index 2d836f88..4b244efa 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/entity/knowledge/KnowledgeAttach.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/entity/knowledge/KnowledgeAttach.java @@ -37,6 +37,9 @@ public class KnowledgeAttach extends BaseEntity { */ private String docId; + /** SHA-256 content digest used for upload idempotency. */ + private String fileHash; + /** * 附件名称 */ diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/entity/knowledge/KnowledgeFragment.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/entity/knowledge/KnowledgeFragment.java index d184a10a..1fa84aca 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/entity/knowledge/KnowledgeFragment.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/entity/knowledge/KnowledgeFragment.java @@ -27,6 +27,11 @@ public class KnowledgeFragment extends BaseEntity { @TableId(value = "id") private Long id; + /** + * 向量库片段ID(与向量库中的 fid 元数据对应,用于向量定位与混合检索融合) + */ + private String fid; + /** * 文档ID-用于关联文本块信息 */ diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/vo/knowledge/KnowledgeFragmentVo.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/vo/knowledge/KnowledgeFragmentVo.java index 45b4a9ab..a0c7bd8b 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/vo/knowledge/KnowledgeFragmentVo.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/vo/knowledge/KnowledgeFragmentVo.java @@ -30,6 +30,11 @@ public class KnowledgeFragmentVo implements Serializable { @ExcelProperty(value = "主键") private Long id; + /** + * 向量库片段ID + */ + private String fid; + /** * 文档ID-用于关联文本块信息 */ diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/factory/VectorStoreStrategyFactory.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/factory/VectorStoreStrategyFactory.java index a66ea66c..4c4fe536 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/factory/VectorStoreStrategyFactory.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/factory/VectorStoreStrategyFactory.java @@ -10,6 +10,7 @@ import org.ruoyi.service.vector.impl.MilvusVectorStoreStrategy; import org.ruoyi.service.vector.impl.QdrantVectorStoreStrategy; import org.ruoyi.service.vector.impl.WeaviateVectorStoreStrategy; import org.springframework.stereotype.Component; +import org.ruoyi.common.core.exception.ServiceException; import java.util.HashMap; import java.util.Map; @@ -45,14 +46,20 @@ public class VectorStoreStrategyFactory { * 获取当前配置的向量库策略 */ public VectorStoreService getStrategy() { - String vectorStoreType = vectorStoreProperties.getType(); + return getStrategy(null); + } + + public VectorStoreService getStrategy(String requestedType) { + String vectorStoreType = requestedType; + if (vectorStoreType == null || vectorStoreType.trim().isEmpty()) { + vectorStoreType = vectorStoreProperties.getType(); + } if (vectorStoreType == null || vectorStoreType.trim().isEmpty()) { vectorStoreType = "weaviate"; // 默认使用weaviate } VectorStoreService strategy = strategies.get(vectorStoreType.toLowerCase()); if (strategy == null) { - log.warn("未找到向量库策略: {}, 使用默认策略: weaviate", vectorStoreType); - strategy = strategies.get("weaviate"); + throw new ServiceException("不支持的向量库类型: " + vectorStoreType); } log.debug("使用向量库策略: {}", vectorStoreType); return strategy; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mapper/knowledge/KnowledgeAttachMapper.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mapper/knowledge/KnowledgeAttachMapper.java index 05b4d780..f6c7aeff 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mapper/knowledge/KnowledgeAttachMapper.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mapper/knowledge/KnowledgeAttachMapper.java @@ -6,6 +6,8 @@ import org.apache.ibatis.annotations.Select; import org.ruoyi.domain.entity.knowledge.KnowledgeAttach; import org.ruoyi.domain.vo.knowledge.KnowledgeAttachVo; import org.ruoyi.common.mybatis.core.mapper.BaseMapperPlus; +import java.util.List; +import java.util.Map; /** * 知识库附件Mapper接口 @@ -21,4 +23,10 @@ public interface KnowledgeAttachMapper extends BaseMapperPlusSELECT knowledge_id AS knowledgeId, COUNT(*) AS documentCount " + + "FROM knowledge_attach WHERE knowledge_id IN " + + "#{id} " + + "GROUP BY knowledge_id") + List> countByKnowledgeIds(@Param("knowledgeIds") List knowledgeIds); } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mapper/knowledge/KnowledgeFragmentMapper.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mapper/knowledge/KnowledgeFragmentMapper.java index 304bb7d5..f710b9ac 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mapper/knowledge/KnowledgeFragmentMapper.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mapper/knowledge/KnowledgeFragmentMapper.java @@ -34,7 +34,7 @@ public interface KnowledgeFragmentMapper extends BaseMapperPlus") List selectFragmentCountByDocIds(@Param("docIds") List docIds); @Select("