本地 RAG 知识库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1739 lines
94 KiB

package com.wok.supportbot.config;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 数据库初始化配置
* 应用启动时检查并创建必要的表、迁移列、同步种子数据。
*
* ⚠️ 开发规范:任何涉及建表、增删改列、初始数据的变更,
* 必须同时更新本类(init 方法)和 init-database.sql(手动备用脚本),
* 保持两者一致,否则新部署环境会丢失变更。
*/
@Component
@Slf4j
public class DatabaseInitConfig {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired(required = false)
private PasswordEncoder passwordEncoder;
@Value("${knowledge.vector.dimension:1024}")
private int vectorDimension;
@PostConstruct
public void init() {
log.info("========== 数据库初始化开始 ==========");
// ==================== 核心业务表 ====================
safeInit("创建聊天消息表 chat_message", () -> {
if (!checkTableExists("chat_message")) {
createChatMessageTable();
}
});
safeInit("创建知识库分类表 knowledge_category", () -> {
if (!checkTableExists("knowledge_category")) {
createCategoryTable();
}
});
safeInit("创建知识文档表 knowledge_document", () -> {
if (!checkTableExists("knowledge_document")) {
createDocumentTable();
} else {
fixTagsDefaultValue();
}
});
// 以下迁移方法始终执行(幂等),确保新旧环境都拥有所有列
safeInit("迁移 knowledge_document.content_hash 列", this::addContentHashColumn);
safeInit("迁移 knowledge_document.enabled 列", this::addDocumentEnabledColumn);
safeInit("迁移 knowledge_document.extra_config 列", this::addDocumentExtraConfigColumn);
safeInit("迁移 knowledge_document.file_path 列", this::addDocumentFilePathColumn);
safeInit("创建客服角色表 customer_service_role", () -> {
if (!checkTableExists("customer_service_role")) {
createCustomerServiceRoleTable();
} else {
dropRoleModelColumn();
}
});
safeInit("创建客服角色知识库关联表 customer_service_role_category", () -> {
if (!checkTableExists("customer_service_role_category")) {
createCustomerServiceRoleCategoryTable();
}
});
safeInit("创建客服账号表 customer_account", () -> {
if (!checkTableExists("customer_account")) {
createCustomerAccountTable();
}
});
safeInit("创建会话归属表 conversation_session", () -> {
if (!checkTableExists("conversation_session")) {
createConversationSessionTable();
}
});
safeInit("迁移 conversation_session.external_account_id 列", this::addConversationSessionExternalAccountIdColumn);
safeInit("迁移 customer_service_role.allowed_mcp_tools 列", this::addRoleAllowedMcpToolsColumn);
safeInit("同步默认客服角色", this::syncDefaultCustomerServiceRoles);
safeInit("同步默认客服账号", this::syncDefaultCustomerAccounts);
// ==================== AI 模型配置表 ====================
safeInit("创建 AI 模型配置表 ai_model_config", () -> {
if (!checkTableExists("ai_model_config")) {
createAiModelConfigTable();
}
});
// ==================== P0 阶段新增表 ====================
// P0-004: 内容安全过滤
safeInit("创建敏感词表 sensitive_word", () -> {
if (!checkTableExists("sensitive_word")) {
createSensitiveWordTable();
}
});
safeInit("创建内容审计日志表 content_audit_log", () -> {
if (!checkTableExists("content_audit_log")) {
createContentAuditLogTable();
}
});
// P0-002: 用户反馈
safeInit("创建消息反馈表 message_feedback", () -> {
if (!checkTableExists("message_feedback")) {
createMessageFeedbackTable();
} else {
addMessageFeedbackProcessedColumns();
}
});
// P0-002-EXT: FAQ 反馈维护关联
safeInit("创建 FAQ 反馈维护关联表 faq_feedback_link", () -> {
if (!checkTableExists("faq_feedback_link")) {
createFaqFeedbackLinkTable();
}
});
// P0-003: FAQ 知识库
safeInit("创建 FAQ 知识库表 knowledge_faq", () -> {
if (!checkTableExists("knowledge_faq")) {
createKnowledgeFaqTable();
} else {
addFaqCategoryIdColumn();
addKnowledgeFaqFeedbackColumns();
}
});
safeInit("创建 FAQ 向量索引表 faq_embedding", () -> {
if (!checkTableExists("faq_embedding")) {
createFaqEmbeddingTable();
}
});
// ==================== P1 阶段新增表 ====================
// P1-001: 用户认证与多用户管理
safeInit("创建系统用户表 sys_user", () -> {
if (!checkTableExists("sys_user")) {
createSysUserTable();
}
});
safeInit("创建系统角色表 sys_role", () -> {
if (!checkTableExists("sys_role")) {
createSysRoleTable();
}
});
safeInit("创建系统权限表 sys_permission", () -> {
if (!checkTableExists("sys_permission")) {
createSysPermissionTable();
}
});
safeInit("创建用户角色关联表 sys_user_role", () -> {
if (!checkTableExists("sys_user_role")) {
createSysUserRoleTable();
}
});
safeInit("迁移 chat_message.user_id 列", this::addChatMessageUserIdColumn);
safeInit("同步默认系统角色", this::syncDefaultSysRoles);
safeInit("同步默认管理员账号", this::syncDefaultAdminUser);
// P1-002: 运营数据分析看板
safeInit("创建 RAG 命中记录表 rag_hit_log", () -> {
if (!checkTableExists("rag_hit_log")) {
createRagHitLogTable();
}
});
safeInit("创建看板快照表 dashboard_snapshot", () -> {
if (!checkTableExists("dashboard_snapshot")) {
createDashboardSnapshotTable();
}
});
safeInit("创建 LLM 调用追踪表 llm_call_trace", () -> {
if (!checkTableExists("llm_call_trace")) {
createLlmCallTraceTable();
}
});
safeInit("迁移 llm_call_trace 扩展列", this::addLlmCallTraceExtensionColumns);
// P1-003: API 开放平台
safeInit("创建 API Key 表 api_key", () -> {
if (!checkTableExists("api_key")) {
createApiKeyTable();
}
});
safeInit("创建 Webhook 配置表 webhook_config", () -> {
if (!checkTableExists("webhook_config")) {
createWebhookConfigTable();
}
});
// ==================== MCP Server 配置 ====================
safeInit("创建 MCP Server 配置表 mcp_server_config", () -> {
if (!checkTableExists("mcp_server_config")) {
createMcpServerConfigTable();
}
});
// 迁移 mcp_server_config.headers 列(旧表补加自定义请求头列,用于鉴权场景)
safeInit("迁移 mcp_server_config.headers 列", this::addMcpServerConfigHeadersColumn);
// P0-001: 混合检索 - 为 vector_store 添加全文检索列
safeInit("初始化 vector_store 全文检索", this::initVectorStoreFullTextSearch);
// 迁移 vector_store.create_time 列(Spring AI 自动建表时不包含此列)
safeInit("迁移 vector_store.create_time 列", this::addVectorStoreCreateTimeColumn);
// ==================== 清理租户相关数据 ====================
safeInit("清理租户表和 tenant_id 列", this::cleanupTenantData);
// ==================== API Key 角色绑定 ====================
safeInit("api_key 新增 role_ids 列", this::addApiKeyRoleIdsColumn);
// ==================== 系统配置 ====================
safeInit("创建系统配置表 system_config", () -> {
if (!checkTableExists("system_config")) {
createSystemConfigTable();
}
});
safeInit("同步默认系统配置", this::syncDefaultSystemConfigs);
// 为所有表添加注释(幂等,可重复执行)
safeInit("应用数据库表注释", this::applyTableComments);
// ==================== 初始化结果验证 ====================
verifyInitialization();
log.info("========== 数据库初始化完成 ==========");
}
/**
* 安全执行初始化步骤,单个步骤失败不影响其他步骤
*/
private void safeInit(String description, Runnable action) {
try {
action.run();
} catch (Exception e) {
log.error("数据库初始化步骤失败 [{}]: {}", description, e.getMessage(), e);
}
}
/**
* 初始化完成后验证所有预期的表是否存在,汇总报告缺失表
*/
private void verifyInitialization() {
String[] expectedTables = {
"chat_message", "knowledge_category", "knowledge_document",
"customer_service_role", "customer_service_role_category",
"customer_account", "conversation_session", "ai_model_config",
"sensitive_word", "content_audit_log", "message_feedback",
"knowledge_faq", "faq_embedding", "faq_feedback_link",
"sys_user", "sys_role", "sys_permission", "sys_user_role",
"rag_hit_log", "dashboard_snapshot",
"api_key", "webhook_config",
"mcp_server_config",
"system_config",
"llm_call_trace"
};
java.util.List<String> missingTables = new java.util.ArrayList<>();
for (String table : expectedTables) {
if (!checkTableExists(table)) {
missingTables.add(table);
}
}
if (missingTables.isEmpty()) {
log.info("数据库初始化验证通过,所有 {} 张表均已创建", expectedTables.length);
} else {
log.error("⚠️ 数据库初始化验证失败!以下 {} 张表缺失: {}", missingTables.size(), missingTables);
log.error("请检查上方错误日志,修复后重启应用");
}
}
private boolean checkTableExists(String tableName) {
try {
String sql = "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = current_schema() AND table_name = ?";
Integer count = jdbcTemplate.queryForObject(sql, Integer.class, tableName.toLowerCase());
return count != null && count > 0;
} catch (Exception e) {
return false;
}
}
private void createChatMessageTable() {
String sql = """
CREATE TABLE IF NOT EXISTS chat_message (
id BIGSERIAL PRIMARY KEY,
conversation_id VARCHAR(64) NOT NULL,
message_type VARCHAR(20) NOT NULL,
content TEXT NOT NULL,
metadata JSONB DEFAULT '{}' NOT NULL,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
is_delete BOOLEAN DEFAULT FALSE NOT NULL
)
""";
jdbcTemplate.execute(sql);
// 创建索引
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_chat_message_conversation_id ON chat_message (conversation_id)");
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_chat_message_create_time ON chat_message (create_time DESC)");
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_chat_message_type ON chat_message (message_type)");
}
private void createCategoryTable() {
String sql = """
CREATE TABLE IF NOT EXISTS knowledge_category (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
description TEXT,
parent_id BIGINT DEFAULT 0 NOT NULL,
sort_order INTEGER DEFAULT 0 NOT NULL,
document_count INTEGER DEFAULT 0 NOT NULL,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
is_delete BOOLEAN DEFAULT FALSE NOT NULL
)
""";
jdbcTemplate.execute(sql);
// 创建索引
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_knowledge_category_parent ON knowledge_category (parent_id)");
}
private void createDocumentTable() {
String sql = """
CREATE TABLE IF NOT EXISTS knowledge_document (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(500) NOT NULL,
source_name VARCHAR(500),
file_type VARCHAR(20) NOT NULL,
file_size BIGINT DEFAULT 0 NOT NULL,
file_path VARCHAR(500),
content TEXT,
category_id BIGINT DEFAULT 0 NOT NULL,
tags JSONB DEFAULT '{}' NOT NULL,
chunk_count INTEGER DEFAULT 0 NOT NULL,
status VARCHAR(20) DEFAULT 'PROCESSING' NOT NULL,
error_message TEXT,
content_hash VARCHAR(64),
enabled BOOLEAN DEFAULT TRUE NOT NULL,
extra_config JSONB DEFAULT '{}',
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
is_delete BOOLEAN DEFAULT FALSE NOT NULL
)
""";
jdbcTemplate.execute(sql);
// 创建索引
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_knowledge_document_category ON knowledge_document (category_id)");
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_knowledge_document_status ON knowledge_document (status)");
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_knowledge_document_create_time ON knowledge_document (create_time DESC)");
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_knowledge_document_content_hash ON knowledge_document (content_hash)");
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_knowledge_document_enabled ON knowledge_document (enabled)");
}
private void createCustomerServiceRoleTable() {
String sql = """
CREATE TABLE IF NOT EXISTS customer_service_role (
id BIGSERIAL PRIMARY KEY,
role_key VARCHAR(64) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
description TEXT,
prompt TEXT,
enabled BOOLEAN DEFAULT TRUE NOT NULL,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
is_delete BOOLEAN DEFAULT FALSE NOT NULL
)
""";
jdbcTemplate.execute(sql);
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_customer_service_role_enabled ON customer_service_role (enabled)");
}
private void createCustomerServiceRoleCategoryTable() {
String sql = """
CREATE TABLE IF NOT EXISTS customer_service_role_category (
id BIGSERIAL PRIMARY KEY,
role_id BIGINT NOT NULL,
category_id BIGINT NOT NULL,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
is_delete BOOLEAN DEFAULT FALSE NOT NULL,
UNIQUE (role_id, category_id)
)
""";
jdbcTemplate.execute(sql);
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_customer_service_role_category_role ON customer_service_role_category (role_id)");
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_customer_service_role_category_category ON customer_service_role_category (category_id)");
}
private void createCustomerAccountTable() {
String sql = """
CREATE TABLE IF NOT EXISTS customer_account (
id BIGSERIAL PRIMARY KEY,
account_key VARCHAR(64) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
description TEXT,
role_id BIGINT,
enabled BOOLEAN DEFAULT TRUE NOT NULL,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
is_delete BOOLEAN DEFAULT FALSE NOT NULL
)
""";
jdbcTemplate.execute(sql);
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_customer_account_role ON customer_account (role_id)");
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_customer_account_enabled ON customer_account (enabled, id)");
}
private void createConversationSessionTable() {
String sql = """
CREATE TABLE IF NOT EXISTS conversation_session (
conversation_id VARCHAR(64) PRIMARY KEY,
account_id BIGINT,
external_account_id VARCHAR(128),
role_id BIGINT,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
)
""";
jdbcTemplate.execute(sql);
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_conversation_session_account ON conversation_session (account_id)");
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_conversation_session_external_account ON conversation_session (external_account_id)");
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_conversation_session_role ON conversation_session (role_id)");
}
private void createAiModelConfigTable() {
String sql = """
CREATE TABLE IF NOT EXISTS ai_model_config (
id BIGINT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
app_type VARCHAR(32) NOT NULL,
provider VARCHAR(64) NOT NULL,
api_key VARCHAR(256),
model_name VARCHAR(128) NOT NULL,
temperature DOUBLE PRECISION DEFAULT 0.7,
max_tokens INTEGER DEFAULT 2048,
base_url VARCHAR(256),
extra_config JSONB DEFAULT '{}' NOT NULL,
is_active BOOLEAN DEFAULT FALSE NOT NULL,
priority INTEGER DEFAULT 0 NOT NULL,
description VARCHAR(500),
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
is_delete BOOLEAN NOT NULL DEFAULT FALSE
)
""";
jdbcTemplate.execute(sql);
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_ai_model_config_app_type ON ai_model_config (app_type)");
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_ai_model_config_active ON ai_model_config (is_active) WHERE is_delete = FALSE");
}
private void addConversationSessionExternalAccountIdColumn() {
try {
String checkSql = "SELECT COUNT(*) FROM information_schema.columns WHERE table_name = 'conversation_session' AND column_name = 'external_account_id'";
Integer count = jdbcTemplate.queryForObject(checkSql, Integer.class);
if (count != null && count == 0) {
jdbcTemplate.execute("ALTER TABLE conversation_session ADD COLUMN external_account_id VARCHAR(128)");
}
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_conversation_session_external_account ON conversation_session (external_account_id)");
} catch (Exception e) {
log.warn("add conversation_session.external_account_id failed: {}", e.getMessage());
}
}
/**
* 为 customer_service_role 表添加 allowed_mcp_tools 列(角色 MCP 工具权限控制)
* 幂等:已有列则跳过
*/
private void addRoleAllowedMcpToolsColumn() {
try {
String checkSql = "SELECT COUNT(*) FROM information_schema.columns WHERE table_name = 'customer_service_role' AND column_name = 'allowed_mcp_tools'";
Integer count = jdbcTemplate.queryForObject(checkSql, Integer.class);
if (count != null && count == 0) {
log.info("添加 customer_service_role.allowed_mcp_tools 列");
jdbcTemplate.execute("ALTER TABLE customer_service_role ADD COLUMN allowed_mcp_tools JSONB DEFAULT '[]'");
}
} catch (Exception e) {
log.error("添加 customer_service_role.allowed_mcp_tools 列失败", e);
}
}
private void syncDefaultCustomerServiceRoles() {
upsertDefaultRole("general", "客服", "用户咨询、业务办理、常见问题、问题受理与进度说明",
"""
职责:回答用户关于业务办理、服务流程、常见问题、问题受理和进度说明的问题。
要求:优先依据知识库回答;资料不足时说明无法确认,并引导用户补充必要信息;涉及财务、行政制度细节时不要猜测,应提示转对应角色处理。
""");
upsertDefaultRole("finance", "财务", "付款、退款、发票、对账、报销、结算与费用规则",
"""
职责:回答付款、退款、发票、对账、报销、结算、费用规则相关问题。
要求:金额、账户、票据、时间节点必须严谨;知识库没有依据时不得编造政策;需要用户提供单号、金额、日期、发票抬头等关键信息。
""");
upsertDefaultRole("administration", "行政", "办公制度、行政流程、资产、会议、考勤、用章、采购与后勤",
"""
职责:回答办公制度、行政流程、资产、会议、考勤、入职、用章、采购、后勤等问题。
要求:优先依据公司制度和流程文件回答;涉及审批权限、特殊例外或未覆盖场景时提示按制度提交申请或联系行政负责人。
""");
retireObsoleteDefaultRoles();
}
private void syncDefaultCustomerAccounts() {
upsertDefaultAccount("service", "客服账号", "默认客服账号", "general");
upsertDefaultAccount("finance", "财务账号", "默认财务账号", "finance");
upsertDefaultAccount("administration", "行政账号", "默认行政账号", "administration");
}
private void upsertDefaultRole(String roleKey, String name, String description, String prompt) {
jdbcTemplate.update("""
INSERT INTO customer_service_role (role_key, name, description, prompt)
VALUES (?, ?, ?, ?)
ON CONFLICT (role_key)
DO NOTHING
""", roleKey, name, description, prompt);
}
private void upsertDefaultAccount(String accountKey, String name, String description, String roleKey) {
Long roleId = jdbcTemplate.queryForObject("""
SELECT id FROM customer_service_role
WHERE role_key = ? AND is_delete = false
LIMIT 1
""", Long.class, roleKey);
jdbcTemplate.update("""
INSERT INTO customer_account (account_key, name, description, role_id)
VALUES (?, ?, ?, ?)
ON CONFLICT (account_key)
DO UPDATE SET name = EXCLUDED.name,
description = EXCLUDED.description,
role_id = EXCLUDED.role_id,
enabled = true,
is_delete = false,
update_time = CURRENT_TIMESTAMP
""", accountKey, name, description, roleId);
}
private void retireObsoleteDefaultRoles() {
jdbcTemplate.update("""
UPDATE customer_service_role
SET is_delete = true, enabled = false, update_time = CURRENT_TIMESTAMP
WHERE role_key IN ('after_sale', 'logistics', 'product')
""");
jdbcTemplate.update("""
UPDATE customer_service_role_category
SET is_delete = true
WHERE role_id IN (
SELECT id FROM customer_service_role
WHERE role_key IN ('after_sale', 'logistics', 'product')
)
""");
}
private void fixTagsDefaultValue() {
try {
// 检查当前默认值是否为数组
String checkSql = "SELECT column_default FROM information_schema.columns WHERE table_name = 'knowledge_document' AND column_name = 'tags'";
String currentDefault = jdbcTemplate.queryForObject(checkSql, String.class);
if (currentDefault != null && currentDefault.contains("[]")) {
log.info("修复 knowledge_document.tags 默认值");
jdbcTemplate.execute("ALTER TABLE knowledge_document ALTER COLUMN tags SET DEFAULT '{}'");
// 将已有的 '[]' 更新为 '{}'
jdbcTemplate.execute("UPDATE knowledge_document SET tags = '{}' WHERE tags = '[]' OR tags IS NULL");
}
} catch (Exception e) {
log.warn("修复 tags 默认值时出错(可能已修复)", e);
}
}
/**
* 清理已废弃的 customer_service_role.model 列(per-role 模型功能已撤除)。
* 幂等:DROP COLUMN IF EXISTS,列不存在时不做任何事。
*/
private void dropRoleModelColumn() {
try {
jdbcTemplate.execute("ALTER TABLE customer_service_role DROP COLUMN IF EXISTS model");
} catch (Exception e) {
log.warn("清理 model 列时出错", e);
}
}
/**
* 自动添加 content_hash 列(二期去重功能新增字段)
*/
private void addContentHashColumn() {
try {
String checkSql = "SELECT COUNT(*) FROM information_schema.columns WHERE table_name = 'knowledge_document' AND column_name = 'content_hash'";
Integer count = jdbcTemplate.queryForObject(checkSql, Integer.class);
if (count != null && count == 0) {
log.info("添加 knowledge_document.content_hash 列");
jdbcTemplate.execute("ALTER TABLE knowledge_document ADD COLUMN content_hash VARCHAR(64)");
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_knowledge_document_content_hash ON knowledge_document (content_hash)");
}
} catch (Exception e) {
log.error("添加 knowledge_document.content_hash 列失败,请手动执行: ALTER TABLE knowledge_document ADD COLUMN content_hash VARCHAR(64)", e);
}
}
/**
* P1-2.1: 自动添加 enabled 列(文档启用/禁用功能)
* 幂等:已有列则跳过
*/
private void addDocumentEnabledColumn() {
try {
String checkSql = "SELECT COUNT(*) FROM information_schema.columns WHERE table_name = 'knowledge_document' AND column_name = 'enabled'";
Integer count = jdbcTemplate.queryForObject(checkSql, Integer.class);
if (count != null && count == 0) {
log.info("添加 knowledge_document.enabled 列");
jdbcTemplate.execute("ALTER TABLE knowledge_document ADD COLUMN enabled BOOLEAN DEFAULT TRUE NOT NULL");
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_knowledge_document_enabled ON knowledge_document (enabled)");
}
} catch (Exception e) {
log.error("添加 knowledge_document.enabled 列失败,请手动执行: ALTER TABLE knowledge_document ADD COLUMN enabled BOOLEAN DEFAULT TRUE NOT NULL", e);
}
}
/**
* P1-2.5: 自动添加 extra_config 列(per-doc 分块参数存储)
* 幂等:已有列则跳过
*/
private void addDocumentExtraConfigColumn() {
try {
String checkSql = "SELECT COUNT(*) FROM information_schema.columns WHERE table_name = 'knowledge_document' AND column_name = 'extra_config'";
Integer count = jdbcTemplate.queryForObject(checkSql, Integer.class);
if (count != null && count == 0) {
log.info("添加 knowledge_document.extra_config 列");
jdbcTemplate.execute("ALTER TABLE knowledge_document ADD COLUMN extra_config JSONB DEFAULT '{}'");
}
} catch (Exception e) {
log.error("添加 knowledge_document.extra_config 列失败,请手动执行: ALTER TABLE knowledge_document ADD COLUMN extra_config JSONB DEFAULT '{}'", e);
}
}
/**
* 为 knowledge_document 表添加 file_path 列(存储原始文件路径)
*/
private void addDocumentFilePathColumn() {
try {
String checkSql = "SELECT COUNT(*) FROM information_schema.columns WHERE table_name = 'knowledge_document' AND column_name = 'file_path'";
Integer count = jdbcTemplate.queryForObject(checkSql, Integer.class);
if (count != null && count == 0) {
log.info("添加 knowledge_document.file_path 列");
jdbcTemplate.execute("ALTER TABLE knowledge_document ADD COLUMN file_path VARCHAR(500)");
}
} catch (Exception e) {
log.error("添加 knowledge_document.file_path 列失败,请手动执行: ALTER TABLE knowledge_document ADD COLUMN file_path VARCHAR(500)", e);
}
}
// ==================== P0-004: 内容安全过滤 ====================
private void createSensitiveWordTable() {
String sql = """
CREATE TABLE IF NOT EXISTS sensitive_word (
id BIGSERIAL PRIMARY KEY,
word VARCHAR(256) NOT NULL,
category VARCHAR(64) NOT NULL DEFAULT 'custom',
level INTEGER NOT NULL DEFAULT 1,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
remark VARCHAR(512),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
is_delete BOOLEAN DEFAULT FALSE NOT NULL
)
""";
jdbcTemplate.execute(sql);
jdbcTemplate.execute("CREATE UNIQUE INDEX IF NOT EXISTS uk_sensitive_word_word_category ON sensitive_word (word, category) WHERE is_delete = false");
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_sensitive_word_active ON sensitive_word (is_active) WHERE is_delete = false");
}
private void createContentAuditLogTable() {
String sql = """
CREATE TABLE IF NOT EXISTS content_audit_log (
id BIGSERIAL PRIMARY KEY,
session_id VARCHAR(64),
direction VARCHAR(16) NOT NULL,
original_text TEXT NOT NULL,
hit_words JSONB DEFAULT '[]' NOT NULL,
action_taken VARCHAR(32) NOT NULL,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
)
""";
jdbcTemplate.execute(sql);
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_audit_log_created ON content_audit_log (create_time DESC)");
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_audit_log_session ON content_audit_log (session_id)");
}
// ==================== P0-002: 用户反馈 ====================
private void createMessageFeedbackTable() {
String sql = """
CREATE TABLE IF NOT EXISTS message_feedback (
id BIGSERIAL PRIMARY KEY,
message_id VARCHAR(128) NOT NULL,
conversation_id VARCHAR(64) NOT NULL,
feedback_type VARCHAR(16) NOT NULL,
reason_category VARCHAR(64),
reason_comment TEXT,
processed BOOLEAN DEFAULT FALSE NOT NULL,
processed_by BIGINT,
processed_time TIMESTAMP,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
is_delete BOOLEAN DEFAULT FALSE NOT NULL
)
""";
jdbcTemplate.execute(sql);
jdbcTemplate.execute("CREATE UNIQUE INDEX IF NOT EXISTS uk_message_feedback_message ON message_feedback (message_id) WHERE is_delete = false");
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_feedback_conversation ON message_feedback (conversation_id)");
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_feedback_type ON message_feedback (feedback_type) WHERE is_delete = false");
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_feedback_created ON message_feedback (create_time DESC)");
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_feedback_type_reason ON message_feedback (feedback_type, reason_category, processed, create_time)");
}
/**
* 为已存在的 message_feedback 表添加处理状态字段(反馈运营看板需要)
*/
private void addMessageFeedbackProcessedColumns() {
try {
String checkSql = "SELECT COUNT(*) FROM information_schema.columns WHERE table_name = 'message_feedback' AND column_name = 'processed'";
Integer count = jdbcTemplate.queryForObject(checkSql, Integer.class);
if (count != null && count == 0) {
log.info("添加 message_feedback.processed 列");
jdbcTemplate.execute("ALTER TABLE message_feedback ADD COLUMN IF NOT EXISTS processed BOOLEAN DEFAULT FALSE NOT NULL");
}
checkSql = "SELECT COUNT(*) FROM information_schema.columns WHERE table_name = 'message_feedback' AND column_name = 'processed_by'";
count = jdbcTemplate.queryForObject(checkSql, Integer.class);
if (count != null && count == 0) {
log.info("添加 message_feedback.processed_by 列");
jdbcTemplate.execute("ALTER TABLE message_feedback ADD COLUMN IF NOT EXISTS processed_by BIGINT");
}
checkSql = "SELECT COUNT(*) FROM information_schema.columns WHERE table_name = 'message_feedback' AND column_name = 'processed_time'";
count = jdbcTemplate.queryForObject(checkSql, Integer.class);
if (count != null && count == 0) {
log.info("添加 message_feedback.processed_time 列");
jdbcTemplate.execute("ALTER TABLE message_feedback ADD COLUMN IF NOT EXISTS processed_time TIMESTAMP");
}
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_feedback_type_reason ON message_feedback (feedback_type, reason_category, processed, create_time)");
} catch (Exception e) {
log.error("添加 message_feedback 处理状态列失败", e);
}
}
// ==================== P0-002-EXT: FAQ 反馈维护关联 ====================
private void createFaqFeedbackLinkTable() {
String sql = """
CREATE TABLE IF NOT EXISTS faq_feedback_link (
id BIGSERIAL PRIMARY KEY,
feedback_id BIGINT NOT NULL,
faq_id BIGINT,
action_type VARCHAR(32) NOT NULL,
original_question TEXT,
original_answer TEXT,
operator_id BIGINT,
remark TEXT,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
is_delete BOOLEAN DEFAULT FALSE NOT NULL
)
""";
jdbcTemplate.execute(sql);
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_faq_feedback_link_faq ON faq_feedback_link (faq_id)");
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_faq_feedback_link_feedback ON faq_feedback_link (feedback_id)");
}
// ==================== P0-003: FAQ 知识库 ====================
private void createKnowledgeFaqTable() {
String sql = """
CREATE TABLE IF NOT EXISTS knowledge_faq (
id BIGSERIAL PRIMARY KEY,
question TEXT NOT NULL,
answer TEXT NOT NULL,
similar_questions TEXT DEFAULT '[]' NOT NULL,
category VARCHAR(128),
category_id BIGINT,
status VARCHAR(20) NOT NULL DEFAULT 'ENABLED',
priority INTEGER NOT NULL DEFAULT 0,
hit_count BIGINT NOT NULL DEFAULT 0,
source VARCHAR(64) DEFAULT 'manual',
created_from_feedback_id BIGINT,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
is_delete BOOLEAN DEFAULT FALSE NOT NULL
)
""";
jdbcTemplate.execute(sql);
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_faq_status ON knowledge_faq (status) WHERE is_delete = false");
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_faq_category ON knowledge_faq (category)");
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_faq_category_id ON knowledge_faq (category_id)");
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_faq_priority ON knowledge_faq (priority DESC)");
}
private void createFaqEmbeddingTable() {
String sql = """
CREATE TABLE IF NOT EXISTS faq_embedding (
id BIGSERIAL PRIMARY KEY,
faq_id BIGINT NOT NULL,
embedding vector(%d) NOT NULL,
model_name VARCHAR(64) NOT NULL,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
)
""".formatted(vectorDimension);
jdbcTemplate.execute(sql);
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_faq_emb_faq_id ON faq_embedding (faq_id)");
}
/**
* 为已存在的 knowledge_faq 表添加 category_id 列(引用文档管理分类表 knowledge_category)
*/
private void addFaqCategoryIdColumn() {
try {
String checkSql = "SELECT COUNT(*) FROM information_schema.columns WHERE table_name = 'knowledge_faq' AND column_name = 'category_id'";
Integer count = jdbcTemplate.queryForObject(checkSql, Integer.class);
if (count != null && count == 0) {
log.info("添加 knowledge_faq.category_id 列");
jdbcTemplate.execute("ALTER TABLE knowledge_faq ADD COLUMN category_id BIGINT");
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_faq_category_id ON knowledge_faq (category_id)");
}
} catch (Exception e) {
log.error("添加 knowledge_faq.category_id 列失败,请手动执行: ALTER TABLE knowledge_faq ADD COLUMN category_id BIGINT", e);
}
}
/**
* 为已存在的 knowledge_faq 表添加反馈来源追溯字段
*/
private void addKnowledgeFaqFeedbackColumns() {
try {
String checkSql = "SELECT COUNT(*) FROM information_schema.columns WHERE table_name = 'knowledge_faq' AND column_name = 'created_from_feedback_id'";
Integer count = jdbcTemplate.queryForObject(checkSql, Integer.class);
if (count != null && count == 0) {
log.info("添加 knowledge_faq.created_from_feedback_id 列");
jdbcTemplate.execute("ALTER TABLE knowledge_faq ADD COLUMN IF NOT EXISTS created_from_feedback_id BIGINT");
}
} catch (Exception e) {
log.error("添加 knowledge_faq.created_from_feedback_id 列失败", e);
}
}
// ==================== P0-001: 混合检索 - 全文检索 ====================
/**
* 为 vector_store 表添加全文检索支持(tsvector 列 + GIN 索引 + 触发器)
* 幂等操作:已存在则跳过
*/
private void initVectorStoreFullTextSearch() {
try {
// 检查 vector_store 表是否存在
if (!checkTableExists("vector_store")) {
log.debug("vector_store 表尚未创建,跳过全文检索初始化");
return;
}
// 检查 content_tsvector 列是否已存在
String checkSql = "SELECT COUNT(*) FROM information_schema.columns WHERE table_name = 'vector_store' AND column_name = 'content_tsvector'";
Integer count = jdbcTemplate.queryForObject(checkSql, Integer.class);
if (count != null && count > 0) {
return; // 已初始化
}
log.info("为 vector_store 添加全文检索支持");
// 添加 tsvector 列
jdbcTemplate.execute("ALTER TABLE vector_store ADD COLUMN content_tsvector tsvector");
// 填充已有数据
jdbcTemplate.execute("UPDATE vector_store SET content_tsvector = to_tsvector('simple', coalesce(content, '')) WHERE content_tsvector IS NULL");
// GIN 索引
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_vector_store_tsvector ON vector_store USING gin(content_tsvector)");
// 触发器函数
jdbcTemplate.execute("""
CREATE OR REPLACE FUNCTION update_content_tsvector() RETURNS trigger AS $$
BEGIN
NEW.content_tsvector := to_tsvector('simple', coalesce(NEW.content, ''));
RETURN NEW;
END;
$$ LANGUAGE plpgsql
""");
// 绑定触发器
jdbcTemplate.execute("DROP TRIGGER IF EXISTS trg_update_content_tsvector ON vector_store");
jdbcTemplate.execute("""
CREATE TRIGGER trg_update_content_tsvector
BEFORE INSERT OR UPDATE ON vector_store
FOR EACH ROW EXECUTE FUNCTION update_content_tsvector()
""");
log.info("vector_store 全文检索初始化完成");
} catch (Exception e) {
log.warn("初始化 vector_store 全文检索时出错(可稍后手动执行)", e);
}
}
/**
* 为 vector_store 表添加 create_time 列。
* Spring AI 自动建表时不会创建此列,但 DocumentService 查询分块列表时需要按 create_time 排序。
* 幂等:已有列则跳过。
*/
private void addVectorStoreCreateTimeColumn() {
try {
// 表不存在时直接跳过,等待 Spring AI 创建后再迁移
if (!checkTableExists("vector_store")) {
log.debug("vector_store 表尚未创建,跳过 create_time 列迁移");
return;
}
String checkSql = "SELECT COUNT(*) FROM information_schema.columns WHERE table_name = 'vector_store' AND column_name = 'create_time'";
Integer count = jdbcTemplate.queryForObject(checkSql, Integer.class);
if (count != null && count == 0) {
log.info("添加 vector_store.create_time 列");
jdbcTemplate.execute("ALTER TABLE vector_store ADD COLUMN create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL");
// 为已有记录回填创建时间(使用当前时间作为兜底)
jdbcTemplate.execute("UPDATE vector_store SET create_time = CURRENT_TIMESTAMP WHERE create_time IS NULL");
log.info("vector_store.create_time 列添加完成");
}
} catch (Exception e) {
log.error("添加 vector_store.create_time 列失败,请手动执行: ALTER TABLE vector_store ADD COLUMN create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL", e);
}
}
// ==================== P1-001: 用户认证与多用户管理 ====================
private void createSysUserTable() {
String sql = """
CREATE TABLE IF NOT EXISTS sys_user (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(64) NOT NULL UNIQUE,
password VARCHAR(256) NOT NULL,
nickname VARCHAR(100),
email VARCHAR(128),
phone VARCHAR(20),
avatar VARCHAR(512),
enabled BOOLEAN DEFAULT TRUE NOT NULL,
last_login_time TIMESTAMP,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
is_delete BOOLEAN DEFAULT FALSE NOT NULL
)
""";
jdbcTemplate.execute(sql);
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_sys_user_username ON sys_user (username) WHERE is_delete = false");
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_sys_user_enabled ON sys_user (enabled) WHERE is_delete = false");
}
private void createSysRoleTable() {
String sql = """
CREATE TABLE IF NOT EXISTS sys_role (
id BIGSERIAL PRIMARY KEY,
role_key VARCHAR(64) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
description TEXT,
enabled BOOLEAN DEFAULT TRUE NOT NULL,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
is_delete BOOLEAN DEFAULT FALSE NOT NULL
)
""";
jdbcTemplate.execute(sql);
}
private void createSysPermissionTable() {
String sql = """
CREATE TABLE IF NOT EXISTS sys_permission (
id BIGSERIAL PRIMARY KEY,
permission_key VARCHAR(128) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
description TEXT,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
is_delete BOOLEAN DEFAULT FALSE NOT NULL
)
""";
jdbcTemplate.execute(sql);
}
private void createSysUserRoleTable() {
String sql = """
CREATE TABLE IF NOT EXISTS sys_user_role (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
UNIQUE (user_id, role_id)
)
""";
jdbcTemplate.execute(sql);
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_sys_user_role_user ON sys_user_role (user_id)");
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_sys_user_role_role ON sys_user_role (role_id)");
}
/**
* chat_message 表增加 user_id 列(数据隔离迁移,幂等)
*/
private void addChatMessageUserIdColumn() {
try {
String checkSql = "SELECT COUNT(*) FROM information_schema.columns WHERE table_name = 'chat_message' AND column_name = 'user_id'";
Integer count = jdbcTemplate.queryForObject(checkSql, Integer.class);
if (count != null && count == 0) {
log.info("添加 chat_message.user_id 列");
jdbcTemplate.execute("ALTER TABLE chat_message ADD COLUMN user_id BIGINT");
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_chat_message_user_id ON chat_message (user_id)");
}
} catch (Exception e) {
log.error("添加 chat_message.user_id 列失败,请手动执行: ALTER TABLE chat_message ADD COLUMN user_id BIGINT", e);
}
}
/**
* 同步默认系统角色(admin/kb_operator/cs_agent/viewer)
*/
private void syncDefaultSysRoles() {
upsertSysRole("admin", "超级管理员", "系统最高权限,可管理所有功能");
upsertSysRole("kb_operator", "知识库运营", "管理知识库文档、分类、FAQ 等");
upsertSysRole("cs_agent", "客服人员", "处理对话、查看反馈等客服操作");
upsertSysRole("viewer", "只读查看者", "仅查看数据,不可修改");
}
private void upsertSysRole(String roleKey, String name, String description) {
jdbcTemplate.update("""
INSERT INTO sys_role (role_key, name, description)
VALUES (?, ?, ?)
ON CONFLICT (role_key)
DO UPDATE SET name = EXCLUDED.name,
description = EXCLUDED.description,
enabled = true,
is_delete = false,
update_time = CURRENT_TIMESTAMP
""", roleKey, name, description);
}
/**
* 同步默认管理员账号(admin / admin123)
*/
private void syncDefaultAdminUser() {
// 检查是否已有 admin 用户
Long count = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM sys_user WHERE username = 'admin' AND is_delete = false",
Long.class
);
if (count != null && count > 0) return;
log.info("创建默认管理员账号 admin");
// 使用 PasswordEncoder 加密密码(如果可用),否则使用预计算的 BCrypt 哈希
String encodedPassword = (passwordEncoder != null)
? passwordEncoder.encode("admin123")
: "$2a$10$EqKcp1WFKVQIShMPC7B3kuznX9gAZMsVnSNjN0ABNuHVBCpzqABKm";
jdbcTemplate.update("""
INSERT INTO sys_user (username, password, nickname, enabled)
VALUES ('admin', ?, '超级管理员', true)
""", encodedPassword);
// 分配 admin 角色
jdbcTemplate.update("""
INSERT INTO sys_user_role (user_id, role_id)
SELECT u.id, r.id FROM sys_user u, sys_role r
WHERE u.username = 'admin' AND r.role_key = 'admin'
ON CONFLICT DO NOTHING
""");
}
// ==================== P1-002: 运营数据分析看板 ====================
private void createRagHitLogTable() {
String sql = """
CREATE TABLE IF NOT EXISTS rag_hit_log (
id BIGSERIAL PRIMARY KEY,
conversation_id VARCHAR(64),
user_query TEXT NOT NULL,
document_id BIGINT,
document_title VARCHAR(500),
chunk_id VARCHAR(128),
score DOUBLE PRECISION,
search_mode VARCHAR(20),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
)
""";
jdbcTemplate.execute(sql);
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_rag_hit_log_created ON rag_hit_log (create_time DESC)");
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_rag_hit_log_document ON rag_hit_log (document_id)");
}
private void createLlmCallTraceTable() {
String sql = """
CREATE TABLE IF NOT EXISTS llm_call_trace (
id BIGINT PRIMARY KEY,
conversation_id VARCHAR(64),
role_id BIGINT,
role_name VARCHAR(100),
account_id VARCHAR(64),
api_key_id BIGINT,
intent VARCHAR(16),
enable_rag BOOLEAN,
system_prompt TEXT,
global_prompt TEXT,
role_prompt TEXT,
user_message TEXT,
ai_response TEXT,
ai_response_truncated BOOLEAN,
tool_calls_json TEXT,
history_messages_json TEXT,
history_turns INTEGER,
rag_context TEXT,
faq_hit BOOLEAN,
faq_id BIGINT,
faq_question TEXT,
faq_match_type VARCHAR(16),
faq_score DOUBLE PRECISION,
search_mode VARCHAR(20),
hit_count INTEGER,
rag_hits_json TEXT,
model_name VARCHAR(128),
provider VARCHAR(64),
temperature DOUBLE PRECISION,
max_tokens INTEGER,
prompt_tokens INTEGER,
completion_tokens INTEGER,
total_tokens INTEGER,
error_type VARCHAR(32),
error_message TEXT,
latency_ms INTEGER,
status VARCHAR(16),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
)
""";
jdbcTemplate.execute(sql);
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_llm_trace_created ON llm_call_trace (create_time DESC, id DESC)");
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_llm_trace_role_created ON llm_call_trace (role_id, create_time DESC)");
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_llm_trace_conv_created ON llm_call_trace (conversation_id, create_time DESC)");
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_llm_trace_error_type ON llm_call_trace (error_type, create_time DESC)");
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_llm_trace_api_key ON llm_call_trace (api_key_id, create_time DESC)");
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_llm_trace_search_mode ON llm_call_trace (search_mode, create_time DESC)");
}
/**
* 为已存在的 llm_call_trace 表补加扩展列(幂等,供旧部署环境迁移)。
*/
private void addLlmCallTraceExtensionColumns() {
jdbcTemplate.execute("ALTER TABLE llm_call_trace ADD COLUMN IF NOT EXISTS tool_calls_json TEXT");
jdbcTemplate.execute("ALTER TABLE llm_call_trace ADD COLUMN IF NOT EXISTS history_messages_json TEXT");
jdbcTemplate.execute("ALTER TABLE llm_call_trace ADD COLUMN IF NOT EXISTS history_turns INTEGER");
jdbcTemplate.execute("ALTER TABLE llm_call_trace ADD COLUMN IF NOT EXISTS faq_id BIGINT");
jdbcTemplate.execute("ALTER TABLE llm_call_trace ADD COLUMN IF NOT EXISTS faq_question TEXT");
jdbcTemplate.execute("ALTER TABLE llm_call_trace ADD COLUMN IF NOT EXISTS faq_match_type VARCHAR(16)");
jdbcTemplate.execute("ALTER TABLE llm_call_trace ADD COLUMN IF NOT EXISTS faq_score DOUBLE PRECISION");
jdbcTemplate.execute("ALTER TABLE llm_call_trace ADD COLUMN IF NOT EXISTS rag_hits_json TEXT");
jdbcTemplate.execute("ALTER TABLE llm_call_trace ADD COLUMN IF NOT EXISTS prompt_tokens INTEGER");
jdbcTemplate.execute("ALTER TABLE llm_call_trace ADD COLUMN IF NOT EXISTS completion_tokens INTEGER");
jdbcTemplate.execute("ALTER TABLE llm_call_trace ADD COLUMN IF NOT EXISTS total_tokens INTEGER");
jdbcTemplate.execute("ALTER TABLE llm_call_trace ADD COLUMN IF NOT EXISTS error_type VARCHAR(32)");
jdbcTemplate.execute("ALTER TABLE llm_call_trace ADD COLUMN IF NOT EXISTS error_message TEXT");
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_llm_trace_error_type ON llm_call_trace (error_type, create_time DESC)");
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_llm_trace_api_key ON llm_call_trace (api_key_id, create_time DESC)");
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_llm_trace_search_mode ON llm_call_trace (search_mode, create_time DESC)");
}
private void createDashboardSnapshotTable() {
String sql = """
CREATE TABLE IF NOT EXISTS dashboard_snapshot (
id BIGSERIAL PRIMARY KEY,
snapshot_date DATE NOT NULL UNIQUE,
conversation_count INTEGER DEFAULT 0 NOT NULL,
message_count INTEGER DEFAULT 0 NOT NULL,
satisfaction_rate DOUBLE PRECISION DEFAULT 0,
thumbs_up_count INTEGER DEFAULT 0 NOT NULL,
thumbs_down_count INTEGER DEFAULT 0 NOT NULL,
rag_hit_count INTEGER DEFAULT 0 NOT NULL,
rag_miss_count INTEGER DEFAULT 0 NOT NULL,
avg_response_time DOUBLE PRECISION DEFAULT 0,
top_questions JSONB DEFAULT '[]' NOT NULL,
top_hit_documents JSONB DEFAULT '[]' NOT NULL,
miss_questions JSONB DEFAULT '[]' NOT NULL,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
)
""";
jdbcTemplate.execute(sql);
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_snapshot_date ON dashboard_snapshot (snapshot_date DESC)");
}
// ==================== P1-003: API 开放平台 ====================
private void createApiKeyTable() {
String sql = """
CREATE TABLE IF NOT EXISTS api_key (
id BIGSERIAL PRIMARY KEY,
key_value VARCHAR(128) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
description TEXT,
user_id BIGINT,
rate_limit INTEGER DEFAULT 60 NOT NULL,
max_calls BIGINT,
current_calls BIGINT DEFAULT 0 NOT NULL,
expire_time TIMESTAMP,
enabled BOOLEAN DEFAULT TRUE NOT NULL,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
is_delete BOOLEAN DEFAULT FALSE NOT NULL
)
""";
jdbcTemplate.execute(sql);
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_api_key_user ON api_key (user_id)");
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_api_key_enabled ON api_key (enabled) WHERE is_delete = false");
}
private void createWebhookConfigTable() {
String sql = """
CREATE TABLE IF NOT EXISTS webhook_config (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT,
name VARCHAR(100) NOT NULL,
url VARCHAR(512) NOT NULL,
events JSONB DEFAULT '[]' NOT NULL,
enabled BOOLEAN DEFAULT TRUE NOT NULL,
secret VARCHAR(128),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
is_delete BOOLEAN DEFAULT FALSE NOT NULL
)
""";
jdbcTemplate.execute(sql);
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_webhook_user ON webhook_config (user_id)");
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_webhook_enabled ON webhook_config (enabled) WHERE is_delete = false");
}
private void createMcpServerConfigTable() {
String sql = """
CREATE TABLE IF NOT EXISTS mcp_server_config (
id BIGINT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
transport_type VARCHAR(20) NOT NULL DEFAULT 'sse',
server_url VARCHAR(500),
command VARCHAR(500),
args VARCHAR(1000),
env_vars JSONB DEFAULT '{}' NOT NULL,
headers JSONB DEFAULT '{}' NOT NULL,
description VARCHAR(500),
is_active BOOLEAN DEFAULT TRUE NOT NULL,
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
is_delete BOOLEAN NOT NULL DEFAULT FALSE
)
""";
jdbcTemplate.execute(sql);
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_mcp_server_config_active ON mcp_server_config (is_active) WHERE is_delete = FALSE");
}
/**
* 为已存在的 mcp_server_config 表补加 headers 列(JSONB,自定义请求头,用于鉴权场景)。
* 幂等:仅在列不存在时执行 ALTER,保证旧库升级不丢失数据。
*/
private void addMcpServerConfigHeadersColumn() {
if (!checkTableExists("mcp_server_config")) {
log.debug("mcp_server_config 表尚未创建,跳过 headers 迁移");
return;
}
try {
String checkSql = "SELECT COUNT(*) FROM information_schema.columns WHERE table_name = 'mcp_server_config' AND column_name = 'headers'";
Integer count = jdbcTemplate.queryForObject(checkSql, Integer.class);
if (count != null && count == 0) {
log.info("添加 mcp_server_config.headers 列");
jdbcTemplate.execute("ALTER TABLE mcp_server_config ADD COLUMN headers JSONB DEFAULT '{}' NOT NULL");
}
} catch (Exception e) {
log.error("添加 mcp_server_config.headers 列失败", e);
}
}
/**
* 创建系统配置表 system_config(key-value 模式,支持灵活扩展配置项)
*/
private void createSystemConfigTable() {
String sql = """
CREATE TABLE IF NOT EXISTS system_config (
id BIGINT PRIMARY KEY,
config_key VARCHAR(128) NOT NULL UNIQUE,
config_value TEXT NOT NULL DEFAULT '',
description VARCHAR(500),
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
is_delete BOOLEAN NOT NULL DEFAULT FALSE
)
""";
jdbcTemplate.execute(sql);
// 创建序列(仅当不存在时),供种子数据/手动 INSERT 生成雪花 ID 兜底
jdbcTemplate.execute("CREATE SEQUENCE IF NOT EXISTS system_config_id_seq START 1000 NO CYCLE");
jdbcTemplate.execute("ALTER TABLE system_config ALTER COLUMN id SET DEFAULT nextval('system_config_id_seq')");
}
/**
* 同步默认系统配置(种子数据)
* <p>
* 使用 INSERT ... ON CONFLICT DO NOTHING,首次启动写入默认值,已存在不覆盖。
*/
private void syncDefaultSystemConfigs() {
String defaultDisclaimer = """
<p class="csk-disclaimer__title">特别声明 Claim of Confidential</p>\
<p>✓ 本文件内容为 ④ 内部公开 请勿外传:禁止未授权的内部、第三方人员使用与访问</p>\
<p>✓ 非专项必要的业务与项目负责人,收到此内容请立即删除</p>\
<p>✓ 严禁在未经过主管领导审批,发送给无关业务团队与人员</p>\
""";
jdbcTemplate.update("""
INSERT INTO system_config (config_key, config_value, description)
VALUES (?, ?, ?)
ON CONFLICT (config_key) DO NOTHING
""", "disclaimer", defaultDisclaimer, "SDK 聊天窗口底部的保密声明");
// AI 对话全局系统提示词种子数据(为空则不注入)
jdbcTemplate.update("""
INSERT INTO system_config (config_key, config_value, description)
VALUES (?, ?, ?)
ON CONFLICT (config_key) DO NOTHING
""", "ai_system_prompt", "", "AI 对话全局系统提示词(为空则不注入;修改后即时生效,无需重启)");
// AI 推荐问题(suggest-message-list)种子数据
String defaultSuggestionPrompt = """
【推荐问题生成规则】
在回答正文结束后,请严格按以下格式生成 3 条用户可能继续追问的推荐问题:
___SUGGESTIONS___
["推荐问题1", "推荐问题2", "推荐问题3"]
要求:
1. 推荐问题需与当前回答内容相关,覆盖用户可能关心的不同方面
2. 推荐问题是用户可直接点击发送的完整问题文本
3. 必须是合法的 JSON 字符串数组
4. ___SUGGESTIONS___ 是分隔标记,不要在回答正文中出现此标记
""";
jdbcTemplate.update("""
INSERT INTO system_config (config_key, config_value, description)
VALUES (?, ?, ?)
ON CONFLICT (config_key) DO NOTHING
""", "suggestion_enabled", "false", "AI 推荐问题功能开关(true/false)");
jdbcTemplate.update("""
INSERT INTO system_config (config_key, config_value, description)
VALUES (?, ?, ?)
ON CONFLICT (config_key) DO NOTHING
""", "suggestion_prompt", defaultSuggestionPrompt, "AI 推荐问题 Prompt 模板(仅在 suggestion_enabled=true 时生效)");
// LLM 调用追踪记录保留天数(自动清理,默认 30 天)
jdbcTemplate.update("""
INSERT INTO system_config (config_key, config_value, description)
VALUES (?, ?, ?)
ON CONFLICT (config_key) DO NOTHING
""", "llm_trace_retention_days", "30", "LLM 调用追踪记录保留天数(自动清理,默认 30 天)");
}
/**
* 为所有自动创建的表添加注释(COMMENT ON)。
* 所有语句均为幂等操作,可安全重复执行。
*/
private void applyTableComments() {
try {
// ===== chat_message =====
executeComment("TABLE chat_message", "聊天消息表(存储用户与 AI 助手的对话历史)");
executeComment("COLUMN chat_message.id", "主键(雪花算法生成)");
executeComment("COLUMN chat_message.conversation_id", "会话 ID(标识同一次对话)");
executeComment("COLUMN chat_message.message_type", "消息类型: USER(用户消息) / ASSISTANT(AI回复,可能含toolCalls) / SYSTEM(系统消息) / TOOL(MCP工具调用响应)");
executeComment("COLUMN chat_message.content", "消息内容(实际对话文本)");
executeComment("COLUMN chat_message.metadata", "元数据(JSON 格式,存储额外信息)");
executeComment("COLUMN chat_message.create_time", "创建时间");
executeComment("COLUMN chat_message.update_time", "更新时间");
executeComment("COLUMN chat_message.is_delete", "逻辑删除: FALSE=正常 TRUE=已删除");
// ===== knowledge_category =====
executeComment("TABLE knowledge_category", "知识库分类表(支持树形结构)");
executeComment("COLUMN knowledge_category.id", "主键(雪花算法生成)");
executeComment("COLUMN knowledge_category.name", "分类名称");
executeComment("COLUMN knowledge_category.description", "分类描述");
executeComment("COLUMN knowledge_category.parent_id", "父分类 ID(0 表示顶级分类)");
executeComment("COLUMN knowledge_category.sort_order", "排序权重(数值越大越靠前)");
executeComment("COLUMN knowledge_category.document_count", "关联文档数量(冗余字段,定期更新)");
executeComment("COLUMN knowledge_category.create_time", "创建时间");
executeComment("COLUMN knowledge_category.is_delete", "逻辑删除: FALSE=正常 TRUE=已删除");
// ===== knowledge_document =====
executeComment("TABLE knowledge_document", "知识文档表(记录上传的文档元信息)");
executeComment("COLUMN knowledge_document.id", "主键(雪花算法生成)");
executeComment("COLUMN knowledge_document.title", "文档标题");
executeComment("COLUMN knowledge_document.source_name", "原始文件名");
executeComment("COLUMN knowledge_document.file_type", "文件类型: pdf / md / json / txt / word / excel 等");
executeComment("COLUMN knowledge_document.file_size", "文件大小(字节)");
executeComment("COLUMN knowledge_document.content", "原文内容(截断预览)");
executeComment("COLUMN knowledge_document.category_id", "所属分类 ID(0 表示未分类)");
executeComment("COLUMN knowledge_document.tags", "标签(JSON 格式)");
executeComment("COLUMN knowledge_document.chunk_count", "分块数量");
executeComment("COLUMN knowledge_document.status", "处理状态: PROCESSING / READY / FAILED");
executeComment("COLUMN knowledge_document.error_message", "处理失败时的错误信息");
executeComment("COLUMN knowledge_document.content_hash", "内容 SHA-256 哈希值(用于文档去重)");
executeComment("COLUMN knowledge_document.enabled", "是否启用: TRUE=参与RAG检索, FALSE=禁用(不参与检索但保留数据)");
executeComment("COLUMN knowledge_document.extra_config", "分块参数配置(JSONB),存储 per-doc 的 chunkSize/overlap 等");
executeComment("COLUMN knowledge_document.file_path", "原始文件存储路径(相对于存储根目录的相对路径)");
executeComment("COLUMN knowledge_document.create_time", "创建时间");
executeComment("COLUMN knowledge_document.update_time", "更新时间");
executeComment("COLUMN knowledge_document.is_delete", "逻辑删除: FALSE=正常 TRUE=已删除");
// ===== customer_service_role =====
executeComment("TABLE customer_service_role", "客服角色表(定义客服角色的身份与系统提示词)");
executeComment("COLUMN customer_service_role.id", "主键");
executeComment("COLUMN customer_service_role.role_key", "角色标识符(唯一)");
executeComment("COLUMN customer_service_role.name", "角色名称");
executeComment("COLUMN customer_service_role.description", "角色描述");
executeComment("COLUMN customer_service_role.prompt", "系统提示词(角色人设与行为规范)");
executeComment("COLUMN customer_service_role.enabled", "是否启用");
executeComment("COLUMN customer_service_role.create_time", "创建时间");
executeComment("COLUMN customer_service_role.update_time", "更新时间");
executeComment("COLUMN customer_service_role.is_delete", "逻辑删除: FALSE=正常 TRUE=已删除");
executeComment("COLUMN customer_service_role.allowed_mcp_tools", "允许使用的 MCP 工具列表(JSONB 数组,空=不允许,[\"*\"]=全部允许)");
// ===== customer_service_role_category =====
executeComment("TABLE customer_service_role_category", "客服角色知识库关联表(角色与知识库分类的多对多关系)");
executeComment("COLUMN customer_service_role_category.id", "主键");
executeComment("COLUMN customer_service_role_category.role_id", "角色 ID(关联 customer_service_role.id)");
executeComment("COLUMN customer_service_role_category.category_id", "分类 ID(关联 knowledge_category.id)");
executeComment("COLUMN customer_service_role_category.create_time", "创建时间");
executeComment("COLUMN customer_service_role_category.is_delete", "逻辑删除: FALSE=正常 TRUE=已删除");
// ===== customer_account =====
executeComment("TABLE customer_account", "客服账号表(对外暴露的客服入口账号)");
executeComment("COLUMN customer_account.id", "主键");
executeComment("COLUMN customer_account.account_key", "账号标识符(唯一)");
executeComment("COLUMN customer_account.name", "账号名称");
executeComment("COLUMN customer_account.description", "账号描述");
executeComment("COLUMN customer_account.role_id", "关联角色 ID(关联 customer_service_role.id)");
executeComment("COLUMN customer_account.enabled", "是否启用");
executeComment("COLUMN customer_account.create_time", "创建时间");
executeComment("COLUMN customer_account.update_time", "更新时间");
executeComment("COLUMN customer_account.is_delete", "逻辑删除: FALSE=正常 TRUE=已删除");
// ===== conversation_session =====
executeComment("TABLE conversation_session", "会话归属表(记录每个会话归属的账号和角色)");
executeComment("COLUMN conversation_session.conversation_id", "会话 ID(主键)");
executeComment("COLUMN conversation_session.account_id", "归属账号 ID(关联 customer_account.id)");
executeComment("COLUMN conversation_session.role_id", "归属角色 ID(关联 customer_service_role.id)");
executeComment("COLUMN conversation_session.create_time", "创建时间");
executeComment("COLUMN conversation_session.update_time", "更新时间");
// ===== sensitive_word =====
executeComment("TABLE sensitive_word", "敏感词表(DFA 引擎驱动的内容安全过滤)");
executeComment("COLUMN sensitive_word.id", "主键(雪花算法生成)");
executeComment("COLUMN sensitive_word.word", "敏感词内容");
executeComment("COLUMN sensitive_word.category", "分类: politics / porn / abuse / custom,默认 custom");
executeComment("COLUMN sensitive_word.level", "级别: 1=脱敏(MASK) / 2=拦截(BLOCK)");
executeComment("COLUMN sensitive_word.is_active", "是否启用");
executeComment("COLUMN sensitive_word.remark", "备注说明");
executeComment("COLUMN sensitive_word.create_time", "创建时间");
executeComment("COLUMN sensitive_word.update_time", "更新时间");
executeComment("COLUMN sensitive_word.is_delete", "逻辑删除: FALSE=正常 TRUE=已删除");
// ===== content_audit_log =====
executeComment("TABLE content_audit_log", "内容审计日志表(记录敏感词命中事件,只追加不删除)");
executeComment("COLUMN content_audit_log.id", "主键(雪花算法生成)");
executeComment("COLUMN content_audit_log.session_id", "会话 ID(关联 conversation_session)");
executeComment("COLUMN content_audit_log.direction", "检测方向: INPUT(用户输入) / OUTPUT(AI 输出)");
executeComment("COLUMN content_audit_log.original_text", "原始违规内容(截断至前 50 字)");
executeComment("COLUMN content_audit_log.hit_words", "命中词列表(JSON 数组)");
executeComment("COLUMN content_audit_log.action_taken", "采取的动作: PASS(放行) / MASK(脱敏) / BLOCK(拦截)");
executeComment("COLUMN content_audit_log.create_time", "创建时间(事件发生时间)");
// ===== message_feedback =====
executeComment("TABLE message_feedback", "消息反馈表(用户对 AI 回复的点赞/点踩反馈)");
executeComment("COLUMN message_feedback.id", "主键(雪花算法生成)");
executeComment("COLUMN message_feedback.message_id", "AI 消息 ID(唯一索引,重复提交覆盖)");
executeComment("COLUMN message_feedback.conversation_id", "会话 ID");
executeComment("COLUMN message_feedback.feedback_type", "反馈类型: THUMBS_UP(有帮助) / THUMBS_DOWN(没帮助)");
executeComment("COLUMN message_feedback.reason_category", "点踩原因分类: inaccurate / irrelevant / incomplete / other");
executeComment("COLUMN message_feedback.reason_comment", "自由文本补充说明");
executeComment("COLUMN message_feedback.processed", "是否已被运营人员处理");
executeComment("COLUMN message_feedback.processed_by", "处理人 ID");
executeComment("COLUMN message_feedback.processed_time", "处理时间");
executeComment("COLUMN message_feedback.create_time", "创建时间");
executeComment("COLUMN message_feedback.update_time", "更新时间(重复提交时覆盖)");
executeComment("COLUMN message_feedback.is_delete", "逻辑删除: FALSE=正常 TRUE=已删除");
// ===== faq_feedback_link =====
executeComment("TABLE faq_feedback_link", "FAQ 与反馈维护关联表(记录反馈如何转化为 FAQ 维护动作)");
executeComment("COLUMN faq_feedback_link.id", "主键(雪花算法生成)");
executeComment("COLUMN faq_feedback_link.feedback_id", "关联反馈 ID(对应 message_feedback.id)");
executeComment("COLUMN faq_feedback_link.faq_id", "关联 FAQ ID(对应 knowledge_faq.id,创建前可为空)");
executeComment("COLUMN faq_feedback_link.action_type", "维护动作类型: create / append_similar / edit_answer / mark_resolved");
executeComment("COLUMN faq_feedback_link.original_question", "用户原始问题快照");
executeComment("COLUMN faq_feedback_link.original_answer", "AI 原回复快照");
executeComment("COLUMN faq_feedback_link.operator_id", "运营人员 ID");
executeComment("COLUMN faq_feedback_link.remark", "备注说明");
executeComment("COLUMN faq_feedback_link.create_time", "创建时间");
// ===== knowledge_faq =====
executeComment("TABLE knowledge_faq", "FAQ 知识库表(用于意图路由后的精准问答匹配)");
executeComment("COLUMN knowledge_faq.id", "主键(雪花算法生成)");
executeComment("COLUMN knowledge_faq.question", "标准问题");
executeComment("COLUMN knowledge_faq.answer", "标准答案");
executeComment("COLUMN knowledge_faq.similar_questions", "相似问题列表(JSON 字符串数组)");
executeComment("COLUMN knowledge_faq.category", "FAQ 分类(已废弃,改用 category_id)");
executeComment("COLUMN knowledge_faq.category_id", "分类 ID(引用 knowledge_category.id)");
executeComment("COLUMN knowledge_faq.status", "状态: ENABLED(启用) / DISABLED(禁用)");
executeComment("COLUMN knowledge_faq.priority", "优先级(数值越大越优先匹配)");
executeComment("COLUMN knowledge_faq.hit_count", "命中次数统计");
executeComment("COLUMN knowledge_faq.source", "来源: manual(手动录入) / import(批量导入) / feedback_positive(点赞反馈) / feedback_negative(点踩反馈)");
executeComment("COLUMN knowledge_faq.created_from_feedback_id", "由哪条反馈创建(追溯用)");
executeComment("COLUMN knowledge_faq.create_time", "创建时间");
executeComment("COLUMN knowledge_faq.update_time", "更新时间");
executeComment("COLUMN knowledge_faq.is_delete", "逻辑删除: FALSE=正常 TRUE=已删除");
// ===== faq_embedding =====
executeComment("TABLE faq_embedding", "FAQ 向量索引表(存储 FAQ 语义向量,用于相似问题匹配)");
executeComment("COLUMN faq_embedding.id", "主键");
executeComment("COLUMN faq_embedding.faq_id", "关联 FAQ ID(对应 knowledge_faq.id)");
executeComment("COLUMN faq_embedding.embedding", "向量嵌入(维度由 knowledge.vector.dimension 配置)");
executeComment("COLUMN faq_embedding.model_name", "使用的 Embedding 模型名称");
executeComment("COLUMN faq_embedding.create_time", "创建时间");
// ===== sys_user =====
executeComment("TABLE sys_user", "系统用户表(管理后台登录用户)");
executeComment("COLUMN sys_user.id", "主键(雪花算法生成)");
executeComment("COLUMN sys_user.username", "用户名(唯一)");
executeComment("COLUMN sys_user.password", "密码(BCrypt 加密)");
executeComment("COLUMN sys_user.nickname", "昵称");
executeComment("COLUMN sys_user.email", "邮箱");
executeComment("COLUMN sys_user.phone", "手机号");
executeComment("COLUMN sys_user.avatar", "头像URL");
executeComment("COLUMN sys_user.enabled", "是否启用");
executeComment("COLUMN sys_user.last_login_time", "最后登录时间");
executeComment("COLUMN sys_user.create_time", "创建时间");
executeComment("COLUMN sys_user.update_time", "更新时间");
executeComment("COLUMN sys_user.is_delete", "逻辑删除");
// ===== sys_role =====
executeComment("TABLE sys_role", "系统角色表(RBAC 角色定义)");
executeComment("COLUMN sys_role.id", "主键");
executeComment("COLUMN sys_role.role_key", "角色标识符(唯一)");
executeComment("COLUMN sys_role.name", "角色名称");
executeComment("COLUMN sys_role.description", "角色描述");
executeComment("COLUMN sys_role.enabled", "是否启用");
executeComment("COLUMN sys_role.create_time", "创建时间");
executeComment("COLUMN sys_role.update_time", "更新时间");
executeComment("COLUMN sys_role.is_delete", "逻辑删除");
// ===== sys_permission =====
executeComment("TABLE sys_permission", "系统权限表(细粒度权限定义)");
// ===== sys_user_role =====
executeComment("TABLE sys_user_role", "用户-角色关联表(多对多)");
// ===== rag_hit_log =====
executeComment("TABLE rag_hit_log", "RAG 命中记录表(记录每次知识库检索的命中/未命中情况)");
// ===== dashboard_snapshot =====
executeComment("TABLE dashboard_snapshot", "运营看板每日汇总快照");
// ===== api_key =====
executeComment("TABLE api_key", "API Key 表(开放平台认证令牌)");
// ===== webhook_config =====
executeComment("TABLE webhook_config", "Webhook 配置表(事件推送订阅)");
// ===== mcp_server_config =====
executeComment("TABLE mcp_server_config", "MCP Server 配置表(管理外部 MCP Server 连接配置)");
executeComment("COLUMN mcp_server_config.id", "主键(雪花算法生成)");
executeComment("COLUMN mcp_server_config.name", "配置名称");
executeComment("COLUMN mcp_server_config.transport_type", "传输类型: stdio(标准输入输出) / sse(Server-Sent Events)");
executeComment("COLUMN mcp_server_config.server_url", "SSE 模式的 MCP Server URL");
executeComment("COLUMN mcp_server_config.command", "stdio 模式的启动命令");
executeComment("COLUMN mcp_server_config.args", "stdio 模式的命令参数(多个用逗号分隔)");
executeComment("COLUMN mcp_server_config.env_vars", "环境变量(JSONB 键值对)");
executeComment("COLUMN mcp_server_config.headers", "自定义请求头(JSONB 键值对,仅 SSE/HTTP 模式生效,用于鉴权)");
executeComment("COLUMN mcp_server_config.description", "描述说明");
executeComment("COLUMN mcp_server_config.is_active", "是否启用");
executeComment("COLUMN mcp_server_config.create_time", "创建时间");
executeComment("COLUMN mcp_server_config.update_time", "更新时间");
executeComment("COLUMN mcp_server_config.is_delete", "逻辑删除: FALSE=正常 TRUE=已删除");
// ===== ai_model_config =====
executeComment("TABLE ai_model_config", "AI 大模型配置表(管理多套模型配置,按应用类型绑定)");
executeComment("COLUMN ai_model_config.id", "主键(雪花算法生成)");
executeComment("COLUMN ai_model_config.name", "配置名称");
executeComment("COLUMN ai_model_config.app_type", "应用类型: CHAT / EMBEDDING / RAG_REWRITE / RERANK");
executeComment("COLUMN ai_model_config.provider", "模型提供商: dashscope / openai / deepseek / moonshot / zhipu / volcengine 等");
executeComment("COLUMN ai_model_config.api_key", "API Key(数据库加密存储,前端脱敏展示)");
executeComment("COLUMN ai_model_config.model_name", "模型名称(如 qwen-turbo、gpt-4o)");
executeComment("COLUMN ai_model_config.temperature", "温度参数(控制输出随机性,0.0~2.0)");
executeComment("COLUMN ai_model_config.max_tokens", "最大 Token 数(单次生成上限)");
executeComment("COLUMN ai_model_config.base_url", "API 基础地址(可选,用于私有化部署或第三方厂商)");
executeComment("COLUMN ai_model_config.extra_config", "扩展配置(JSONB,存储 topP、dimensions 等自定义参数)");
executeComment("COLUMN ai_model_config.is_active", "是否激活(每种 App 类型只能有一个激活配置)");
executeComment("COLUMN ai_model_config.priority", "优先级(数值越大越优先,多套配置时生效)");
executeComment("COLUMN ai_model_config.description", "配置描述说明");
executeComment("COLUMN ai_model_config.create_time", "创建时间");
executeComment("COLUMN ai_model_config.update_time", "更新时间");
executeComment("COLUMN ai_model_config.is_delete", "逻辑删除: FALSE=正常 TRUE=已删除");
// ===== chat_message.user_id =====
executeComment("COLUMN chat_message.user_id", "所属系统用户ID(数据隔离,SDK调用时为null)");
// ===== api_key.role_ids =====
executeComment("COLUMN api_key.role_ids", "绑定的客服角色 ID 列表(JSONB 数组,空数组=不限制,返回所有启用角色)");
// ===== llm_call_trace =====
executeComment("TABLE llm_call_trace", "LLM 调用追踪表(记录每次 LLM 调用的 system prompt/回复/模型参数/耗时,用于提示词优化调试,append-only)");
executeComment("COLUMN llm_call_trace.id", "主键(雪花算法生成)");
executeComment("COLUMN llm_call_trace.conversation_id", "会话 ID");
executeComment("COLUMN llm_call_trace.role_id", "客服角色 ID(可空)");
executeComment("COLUMN llm_call_trace.role_name", "角色名称快照(角色改名后仍可追溯)");
executeComment("COLUMN llm_call_trace.account_id", "账户 ID(可空,预留租户/个人信息删除权)");
executeComment("COLUMN llm_call_trace.api_key_id", "API Key ID(可空,预留租户隔离)");
executeComment("COLUMN llm_call_trace.intent", "意图:CHAT / CHITCHAT / FAQ / RAG");
executeComment("COLUMN llm_call_trace.enable_rag", "是否启用 RAG 增强");
executeComment("COLUMN llm_call_trace.system_prompt", "最终注入 LLM 的完整 system prompt");
executeComment("COLUMN llm_call_trace.global_prompt", "全局提示词快照(可空)");
executeComment("COLUMN llm_call_trace.role_prompt", "角色提示词快照(可空)");
executeComment("COLUMN llm_call_trace.user_message", "用户原始消息(脱敏后)");
executeComment("COLUMN llm_call_trace.ai_response", "AI 回复(截断,保留头部+尾部)");
executeComment("COLUMN llm_call_trace.ai_response_truncated", "AI 回复是否被截断");
executeComment("COLUMN llm_call_trace.rag_context", "RAG 资料块(可空)");
executeComment("COLUMN llm_call_trace.faq_hit", "是否 FAQ 命中");
executeComment("COLUMN llm_call_trace.search_mode", "检索模式:VECTOR / KEYWORD / HYBRID(可空)");
executeComment("COLUMN llm_call_trace.hit_count", "命中文档数(可空)");
executeComment("COLUMN llm_call_trace.model_name", "模型名称");
executeComment("COLUMN llm_call_trace.provider", "提供商");
executeComment("COLUMN llm_call_trace.temperature", "温度参数");
executeComment("COLUMN llm_call_trace.max_tokens", "最大 Token");
executeComment("COLUMN llm_call_trace.latency_ms", "调用耗时(毫秒)");
executeComment("COLUMN llm_call_trace.status", "状态:COMPLETE / ERROR / CANCEL / FAQ / BYPASS");
executeComment("COLUMN llm_call_trace.create_time", "创建时间");
log.info("数据库表注释已应用");
} catch (Exception e) {
log.warn("应用数据库表注释时出错", e);
}
}
/**
* 执行单条 COMMENT ON 语句
*/
private void executeComment(String target, String comment) {
jdbcTemplate.execute(String.format("COMMENT ON %s IS '%s'", target, comment.replace("'", "''")));
}
// ==================== 清理租户相关数据 ====================
/**
* 清理租户相关数据:删除 tenant 表、移除各表的 tenant_id 列和索引
*/
private void cleanupTenantData() {
// 1. 删除 tenant 表
if (checkTableExists("tenant")) {
jdbcTemplate.execute("DROP TABLE IF EXISTS tenant CASCADE");
log.info("已删除 tenant 表");
}
// 2. 清理 api_key.tenant_id
if (checkTableExists("api_key")) {
try {
jdbcTemplate.execute("DROP INDEX IF EXISTS idx_api_key_tenant_id");
jdbcTemplate.execute("ALTER TABLE api_key DROP COLUMN IF EXISTS tenant_id");
} catch (Exception e) {
log.debug("清理 api_key.tenant_id 跳过: {}", e.getMessage());
}
}
// 3. 清理 customer_service_role.tenant_id
if (checkTableExists("customer_service_role")) {
try {
jdbcTemplate.execute("DROP INDEX IF EXISTS idx_csr_tenant_id");
jdbcTemplate.execute("ALTER TABLE customer_service_role DROP COLUMN IF EXISTS tenant_id");
} catch (Exception e) {
log.debug("清理 customer_service_role.tenant_id 跳过: {}", e.getMessage());
}
}
// 4. 清理 knowledge_category.tenant_id
if (checkTableExists("knowledge_category")) {
try {
jdbcTemplate.execute("DROP INDEX IF EXISTS idx_kc_tenant_id");
jdbcTemplate.execute("ALTER TABLE knowledge_category DROP COLUMN IF EXISTS tenant_id");
} catch (Exception e) {
log.debug("清理 knowledge_category.tenant_id 跳过: {}", e.getMessage());
}
}
// 5. 清理 knowledge_faq.tenant_id
if (checkTableExists("knowledge_faq")) {
try {
jdbcTemplate.execute("DROP INDEX IF EXISTS idx_faq_tenant_id");
jdbcTemplate.execute("ALTER TABLE knowledge_faq DROP COLUMN IF EXISTS tenant_id");
} catch (Exception e) {
log.debug("清理 knowledge_faq.tenant_id 跳过: {}", e.getMessage());
}
}
log.info("租户相关数据清理完成");
}
// ==================== API Key 角色绑定 ====================
/**
* 为 api_key 表添加 role_ids 列(JSONB 数组,存储绑定的客服角色 ID)
*/
private void addApiKeyRoleIdsColumn() {
if (!checkTableExists("api_key")) {
log.debug("api_key 表尚未创建,跳过 role_ids 迁移");
return;
}
try {
String checkSql = "SELECT COUNT(*) FROM information_schema.columns WHERE table_name = 'api_key' AND column_name = 'role_ids'";
Integer count = jdbcTemplate.queryForObject(checkSql, Integer.class);
if (count != null && count == 0) {
log.info("添加 api_key.role_ids 列");
jdbcTemplate.execute("ALTER TABLE api_key ADD COLUMN role_ids JSONB DEFAULT '[]' NOT NULL");
}
} catch (Exception e) {
log.error("添加 api_key.role_ids 列失败", e);
}
}
}