From f9a5516a1a8082a7923c1ba867861e1b9aae7dd9 Mon Sep 17 00:00:00 2001 From: let5sne Date: Sun, 12 Jul 2026 01:46:39 +0800 Subject: [PATCH] feat(personal): add isolated personal knowledge schema --- .../personal/PersonalSchemaContractTest.java | 50 ++++++++++ .../sql/aihr_personal_knowledge_mysql8.sql | 96 +++++++++++++++++++ scripts/reset-dev-db.sh | 1 + 3 files changed, 147 insertions(+) create mode 100644 backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/personal/PersonalSchemaContractTest.java create mode 100644 backend/script/sql/aihr_personal_knowledge_mysql8.sql diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/personal/PersonalSchemaContractTest.java b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/personal/PersonalSchemaContractTest.java new file mode 100644 index 00000000..8b82c89c --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/personal/PersonalSchemaContractTest.java @@ -0,0 +1,50 @@ +package org.dromara.aihr.personal; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Tag; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Locale; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("dev") +class PersonalSchemaContractTest { + + private static final String SCHEMA_FILE = "aihr_personal_knowledge_mysql8.sql"; + + @Test + void personalKnowledgeSchemaIsOwnerScopedAndIndependent() throws IOException { + Path sqlPath = locateSqlDirectory().resolve(SCHEMA_FILE); + assertTrue(Files.isRegularFile(sqlPath), "Missing personal knowledge schema: " + sqlPath); + + String sql = Files.readString(sqlPath).toLowerCase(Locale.ROOT); + assertTrue(sql.contains("create table if not exists `aihr_personal_space`")); + assertTrue(sql.contains("create table if not exists `aihr_personal_item`")); + assertTrue(sql.contains("create table if not exists `aihr_personal_fragment`")); + assertTrue(sql.contains("create table if not exists `aihr_personal_chat_session`")); + assertTrue(sql.contains("create table if not exists `aihr_personal_chat_message`")); + assertTrue(sql.contains("`owner_user_id`"), "Personal records must be owner scoped"); + assertTrue(sql.contains("fulltext key"), "Personal fragments must provide full-text search"); + assertFalse(sql.contains("alter table aihr_knowledge_fragment"), + "Personal schema must not mutate enterprise knowledge tables"); + assertFalse(sql.contains("alter table `aihr_knowledge_fragment`"), + "Personal schema must not mutate enterprise knowledge tables"); + } + + private Path locateSqlDirectory() { + Path current = Path.of("").toAbsolutePath().normalize(); + while (current != null) { + Path candidate = current.resolve("backend/script/sql"); + if (Files.isDirectory(candidate)) { + return candidate; + } + current = current.getParent(); + } + throw new IllegalStateException("Unable to locate backend/script/sql from " + + Path.of("").toAbsolutePath()); + } +} diff --git a/backend/script/sql/aihr_personal_knowledge_mysql8.sql b/backend/script/sql/aihr_personal_knowledge_mysql8.sql new file mode 100644 index 00000000..135c41ef --- /dev/null +++ b/backend/script/sql/aihr_personal_knowledge_mysql8.sql @@ -0,0 +1,96 @@ +-- 个人 AI 助理独立知识空间。 +-- 个人知识仅按 tenant_id + owner_user_id 隔离,不修改或复用企业 aihr_knowledge_* 表。 + +CREATE TABLE IF NOT EXISTS `aihr_personal_space` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', + `tenant_id` varchar(20) NOT NULL DEFAULT '000000' COMMENT '租户编号', + `owner_user_id` bigint NOT NULL COMMENT '个人空间所属用户ID', + `owner_ext_party_id` varchar(100) DEFAULT NULL COMMENT '外部组织主体ID', + `status` varchar(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '空间状态', + `quota_bytes` bigint NOT NULL DEFAULT 524288000 COMMENT '空间配额,默认500MB', + `used_bytes` bigint NOT NULL DEFAULT 0 COMMENT '已使用字节数', + `item_count` int NOT NULL DEFAULT 0 COMMENT '资料数量', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_aihr_personal_space_owner` (`tenant_id`, `owner_user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='个人AI助理知识空间'; + +CREATE TABLE IF NOT EXISTS `aihr_personal_item` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', + `tenant_id` varchar(20) NOT NULL DEFAULT '000000' COMMENT '租户编号', + `owner_user_id` bigint NOT NULL COMMENT '资料所属用户ID', + `owner_ext_party_id` varchar(100) DEFAULT NULL COMMENT '外部组织主体ID', + `space_id` bigint NOT NULL COMMENT '个人空间ID', + `source_type` varchar(30) NOT NULL COMMENT '来源类型: text/file/url', + `title` varchar(500) NOT NULL COMMENT '资料标题', + `source_url` varchar(2000) DEFAULT NULL COMMENT '原始网址', + `oss_id` bigint DEFAULT NULL COMMENT '对象存储ID', + `oss_url` varchar(2000) DEFAULT NULL COMMENT '对象存储访问地址', + `mime_type` varchar(255) DEFAULT NULL COMMENT 'MIME类型', + `size_bytes` bigint NOT NULL DEFAULT 0 COMMENT '资料大小', + `content_hash` varchar(128) DEFAULT NULL COMMENT '内容摘要哈希', + `status` varchar(30) NOT NULL DEFAULT 'PENDING' COMMENT '处理状态', + `attempt_count` int NOT NULL DEFAULT 0 COMMENT '处理尝试次数', + `last_error` varchar(1000) DEFAULT NULL COMMENT '最后错误信息', + `summary` text COMMENT '资料摘要', + `tags_json` longtext COMMENT '标签JSON', + `captured_time` datetime DEFAULT NULL COMMENT '采集完成时间', + `parsed_time` datetime DEFAULT NULL COMMENT '解析完成时间', + `deleted_time` datetime DEFAULT NULL COMMENT '软删除时间', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_aihr_personal_item_owner_status` (`tenant_id`, `owner_user_id`, `status`, `update_time`), + KEY `idx_aihr_personal_item_space` (`space_id`, `id`), + KEY `idx_aihr_personal_item_hash` (`tenant_id`, `owner_user_id`, `content_hash`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='个人AI助理资料'; + +CREATE TABLE IF NOT EXISTS `aihr_personal_fragment` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', + `tenant_id` varchar(20) NOT NULL DEFAULT '000000' COMMENT '租户编号', + `owner_user_id` bigint NOT NULL COMMENT '片段所属用户ID', + `owner_ext_party_id` varchar(100) DEFAULT NULL COMMENT '外部组织主体ID', + `item_id` bigint NOT NULL COMMENT '个人资料ID', + `fragment_index` int NOT NULL COMMENT '片段序号', + `content` text NOT NULL COMMENT '片段内容', + `token_count` int NOT NULL DEFAULT 0 COMMENT '估算Token数', + `embedding_model` varchar(100) DEFAULT NULL COMMENT '向量模型', + `embedding_json` longtext COMMENT '向量JSON', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_aihr_personal_fragment_item_idx` (`item_id`, `fragment_index`), + KEY `idx_aihr_personal_fragment_owner` (`tenant_id`, `owner_user_id`, `item_id`), + FULLTEXT KEY `ft_aihr_personal_fragment_content` (`content`) WITH PARSER ngram +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='个人AI助理知识片段'; + +CREATE TABLE IF NOT EXISTS `aihr_personal_chat_session` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', + `tenant_id` varchar(20) NOT NULL DEFAULT '000000' COMMENT '租户编号', + `owner_user_id` bigint NOT NULL COMMENT '会话所属用户ID', + `owner_ext_party_id` varchar(100) DEFAULT NULL COMMENT '外部组织主体ID', + `title` varchar(255) NOT NULL DEFAULT '新对话' COMMENT '会话标题', + `status` varchar(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '会话状态', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_aihr_personal_chat_session_owner` (`tenant_id`, `owner_user_id`, `status`, `update_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='个人AI助理对话会话'; + +CREATE TABLE IF NOT EXISTS `aihr_personal_chat_message` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', + `tenant_id` varchar(20) NOT NULL DEFAULT '000000' COMMENT '租户编号', + `owner_user_id` bigint NOT NULL COMMENT '消息所属用户ID', + `owner_ext_party_id` varchar(100) DEFAULT NULL COMMENT '外部组织主体ID', + `session_id` bigint NOT NULL COMMENT '对话会话ID', + `role` varchar(20) NOT NULL COMMENT '消息角色: user/assistant/system', + `content` longtext NOT NULL COMMENT '消息内容', + `citations_json` longtext COMMENT '引用来源JSON', + `input_tokens` int NOT NULL DEFAULT 0 COMMENT '输入Token数', + `output_tokens` int NOT NULL DEFAULT 0 COMMENT '输出Token数', + `model_name` varchar(100) DEFAULT NULL COMMENT '模型名称', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`), + KEY `idx_aihr_personal_chat_message_session` (`session_id`, `id`), + KEY `idx_aihr_personal_chat_message_owner` (`tenant_id`, `owner_user_id`, `create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='个人AI助理对话消息'; diff --git a/scripts/reset-dev-db.sh b/scripts/reset-dev-db.sh index c12f6eac..1faf1695 100755 --- a/scripts/reset-dev-db.sh +++ b/scripts/reset-dev-db.sh @@ -20,6 +20,7 @@ docker exec -i wygj-mysql mysql -uroot -proot --default-character-set=utf8mb4 ry docker exec -i wygj-mysql mysql -uroot -proot --default-character-set=utf8mb4 ry-vue < "$ROOT_DIR/backend/script/sql/ry_job.sql" docker exec -i wygj-mysql mysql -uroot -proot --default-character-set=utf8mb4 ry-vue < "$ROOT_DIR/backend/script/sql/ry_workflow.sql" docker exec -i wygj-mysql mysql -uroot -proot --default-character-set=utf8mb4 ry-vue < "$ROOT_DIR/backend/script/sql/aihr_knowledge_mysql8.sql" +docker exec -i wygj-mysql mysql -uroot -proot --default-character-set=utf8mb4 ry-vue < "$ROOT_DIR/backend/script/sql/aihr_personal_knowledge_mysql8.sql" docker exec -i wygj-mysql mysql -uroot -proot --default-character-set=utf8mb4 ry-vue < "$ROOT_DIR/backend/script/sql/aihr_model_mysql8.sql" docker exec -i wygj-mysql mysql -uroot -proot --default-character-set=utf8mb4 ry-vue < "$ROOT_DIR/backend/script/sql/aihr_practice_mysql8.sql" docker exec -i wygj-mysql mysql -uroot -proot --default-character-set=utf8mb4 ry-vue < "$ROOT_DIR/backend/script/sql/aihr_interview_result_mysql8.sql"