|
|
|
@ -27,152 +27,207 @@ public class DatabaseInitConfig { |
|
|
|
|
|
|
|
@PostConstruct |
|
|
|
public void init() { |
|
|
|
try { |
|
|
|
// 检查 chat_message 表是否存在 |
|
|
|
boolean chatMessageTableExists = checkTableExists("chat_message"); |
|
|
|
if (!chatMessageTableExists) { |
|
|
|
log.info("创建聊天消息表 chat_message"); |
|
|
|
log.info("========== 数据库初始化开始 =========="); |
|
|
|
|
|
|
|
// ==================== 核心业务表 ==================== |
|
|
|
|
|
|
|
safeInit("创建聊天消息表 chat_message", () -> { |
|
|
|
if (!checkTableExists("chat_message")) { |
|
|
|
createChatMessageTable(); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
// 检查 knowledge_category 表是否存在 |
|
|
|
boolean categoryTableExists = checkTableExists("knowledge_category"); |
|
|
|
if (!categoryTableExists) { |
|
|
|
log.info("创建知识库分类表 knowledge_category"); |
|
|
|
safeInit("创建知识库分类表 knowledge_category", () -> { |
|
|
|
if (!checkTableExists("knowledge_category")) { |
|
|
|
createCategoryTable(); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
// 检查 knowledge_document 表是否存在 |
|
|
|
boolean documentTableExists = checkTableExists("knowledge_document"); |
|
|
|
if (!documentTableExists) { |
|
|
|
log.info("创建知识文档表 knowledge_document"); |
|
|
|
safeInit("创建知识文档表 knowledge_document", () -> { |
|
|
|
if (!checkTableExists("knowledge_document")) { |
|
|
|
createDocumentTable(); |
|
|
|
} else { |
|
|
|
// 修复已存在表的 tags 默认值(从数组改为对象) |
|
|
|
fixTagsDefaultValue(); |
|
|
|
// 自动添加 content_hash 列(二期新增) |
|
|
|
addContentHashColumn(); |
|
|
|
// P1-2.1: 自动添加 enabled 列(文档启用/禁用) |
|
|
|
addDocumentEnabledColumn(); |
|
|
|
// P1-2.5: 自动添加 extra_config 列(per-doc 分块参数) |
|
|
|
addDocumentExtraConfigColumn(); |
|
|
|
} |
|
|
|
|
|
|
|
boolean roleTableExists = checkTableExists("customer_service_role"); |
|
|
|
if (!roleTableExists) { |
|
|
|
log.info("创建客服角色表 customer_service_role"); |
|
|
|
}); |
|
|
|
// 以下迁移方法始终执行(幂等),确保新旧环境都拥有所有列 |
|
|
|
safeInit("迁移 knowledge_document.content_hash 列", this::addContentHashColumn); |
|
|
|
safeInit("迁移 knowledge_document.enabled 列", this::addDocumentEnabledColumn); |
|
|
|
safeInit("迁移 knowledge_document.extra_config 列", this::addDocumentExtraConfigColumn); |
|
|
|
|
|
|
|
safeInit("创建客服角色表 customer_service_role", () -> { |
|
|
|
if (!checkTableExists("customer_service_role")) { |
|
|
|
createCustomerServiceRoleTable(); |
|
|
|
} else { |
|
|
|
// 清理已废弃的 model 列(per-role 模型功能已撤除,模型统一由模型配置中心管理) |
|
|
|
dropRoleModelColumn(); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
boolean roleCategoryTableExists = checkTableExists("customer_service_role_category"); |
|
|
|
if (!roleCategoryTableExists) { |
|
|
|
log.info("创建客服角色知识库关联表 customer_service_role_category"); |
|
|
|
safeInit("创建客服角色知识库关联表 customer_service_role_category", () -> { |
|
|
|
if (!checkTableExists("customer_service_role_category")) { |
|
|
|
createCustomerServiceRoleCategoryTable(); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
boolean accountTableExists = checkTableExists("customer_account"); |
|
|
|
if (!accountTableExists) { |
|
|
|
log.info("创建客服账号表 customer_account"); |
|
|
|
safeInit("创建客服账号表 customer_account", () -> { |
|
|
|
if (!checkTableExists("customer_account")) { |
|
|
|
createCustomerAccountTable(); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
boolean conversationSessionTableExists = checkTableExists("conversation_session"); |
|
|
|
if (!conversationSessionTableExists) { |
|
|
|
log.info("创建会话归属表 conversation_session"); |
|
|
|
safeInit("创建会话归属表 conversation_session", () -> { |
|
|
|
if (!checkTableExists("conversation_session")) { |
|
|
|
createConversationSessionTable(); |
|
|
|
} |
|
|
|
addConversationSessionExternalAccountIdColumn(); |
|
|
|
syncDefaultCustomerServiceRoles(); |
|
|
|
syncDefaultCustomerAccounts(); |
|
|
|
}); |
|
|
|
safeInit("迁移 conversation_session.external_account_id 列", this::addConversationSessionExternalAccountIdColumn); |
|
|
|
|
|
|
|
safeInit("同步默认客服角色", this::syncDefaultCustomerServiceRoles); |
|
|
|
safeInit("同步默认客服账号", this::syncDefaultCustomerAccounts); |
|
|
|
|
|
|
|
// ==================== AI 模型配置表 ==================== |
|
|
|
|
|
|
|
safeInit("创建 AI 模型配置表 ai_model_config", () -> { |
|
|
|
if (!checkTableExists("ai_model_config")) { |
|
|
|
createAiModelConfigTable(); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
// ==================== P0 阶段新增表 ==================== |
|
|
|
// ==================== P0 阶段新增表 ==================== |
|
|
|
|
|
|
|
// P0-004: 内容安全过滤 |
|
|
|
// P0-004: 内容安全过滤 |
|
|
|
safeInit("创建敏感词表 sensitive_word", () -> { |
|
|
|
if (!checkTableExists("sensitive_word")) { |
|
|
|
log.info("创建敏感词表 sensitive_word"); |
|
|
|
createSensitiveWordTable(); |
|
|
|
} |
|
|
|
}); |
|
|
|
safeInit("创建内容审计日志表 content_audit_log", () -> { |
|
|
|
if (!checkTableExists("content_audit_log")) { |
|
|
|
log.info("创建内容审计日志表 content_audit_log"); |
|
|
|
createContentAuditLogTable(); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
// P0-002: 用户反馈 |
|
|
|
// P0-002: 用户反馈 |
|
|
|
safeInit("创建消息反馈表 message_feedback", () -> { |
|
|
|
if (!checkTableExists("message_feedback")) { |
|
|
|
log.info("创建消息反馈表 message_feedback"); |
|
|
|
createMessageFeedbackTable(); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
// P0-003: FAQ 知识库 |
|
|
|
// P0-003: FAQ 知识库 |
|
|
|
safeInit("创建 FAQ 知识库表 knowledge_faq", () -> { |
|
|
|
if (!checkTableExists("knowledge_faq")) { |
|
|
|
log.info("创建 FAQ 知识库表 knowledge_faq"); |
|
|
|
createKnowledgeFaqTable(); |
|
|
|
} else { |
|
|
|
// 自动添加 category_id 列(引用文档管理分类) |
|
|
|
addFaqCategoryIdColumn(); |
|
|
|
} |
|
|
|
}); |
|
|
|
safeInit("迁移 knowledge_faq.category_id 列", this::addFaqCategoryIdColumn); |
|
|
|
safeInit("创建 FAQ 向量索引表 faq_embedding", () -> { |
|
|
|
if (!checkTableExists("faq_embedding")) { |
|
|
|
log.info("创建 FAQ 向量索引表 faq_embedding"); |
|
|
|
createFaqEmbeddingTable(); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
// ==================== P1 阶段新增表 ==================== |
|
|
|
// ==================== P1 阶段新增表 ==================== |
|
|
|
|
|
|
|
// P1-001: 用户认证与多用户管理 |
|
|
|
// P1-001: 用户认证与多用户管理 |
|
|
|
safeInit("创建系统用户表 sys_user", () -> { |
|
|
|
if (!checkTableExists("sys_user")) { |
|
|
|
log.info("创建系统用户表 sys_user"); |
|
|
|
createSysUserTable(); |
|
|
|
} |
|
|
|
}); |
|
|
|
safeInit("创建系统角色表 sys_role", () -> { |
|
|
|
if (!checkTableExists("sys_role")) { |
|
|
|
log.info("创建系统角色表 sys_role"); |
|
|
|
createSysRoleTable(); |
|
|
|
} |
|
|
|
}); |
|
|
|
safeInit("创建系统权限表 sys_permission", () -> { |
|
|
|
if (!checkTableExists("sys_permission")) { |
|
|
|
log.info("创建系统权限表 sys_permission"); |
|
|
|
createSysPermissionTable(); |
|
|
|
} |
|
|
|
}); |
|
|
|
safeInit("创建用户角色关联表 sys_user_role", () -> { |
|
|
|
if (!checkTableExists("sys_user_role")) { |
|
|
|
log.info("创建用户角色关联表 sys_user_role"); |
|
|
|
createSysUserRoleTable(); |
|
|
|
} |
|
|
|
// chat_message 增加 user_id 列(数据隔离) |
|
|
|
addChatMessageUserIdColumn(); |
|
|
|
// 同步默认角色和管理员账号 |
|
|
|
syncDefaultSysRoles(); |
|
|
|
syncDefaultAdminUser(); |
|
|
|
}); |
|
|
|
safeInit("迁移 chat_message.user_id 列", this::addChatMessageUserIdColumn); |
|
|
|
safeInit("同步默认系统角色", this::syncDefaultSysRoles); |
|
|
|
safeInit("同步默认管理员账号", this::syncDefaultAdminUser); |
|
|
|
|
|
|
|
// P1-002: 运营数据分析看板 |
|
|
|
// P1-002: 运营数据分析看板 |
|
|
|
safeInit("创建 RAG 命中记录表 rag_hit_log", () -> { |
|
|
|
if (!checkTableExists("rag_hit_log")) { |
|
|
|
log.info("创建 RAG 命中记录表 rag_hit_log"); |
|
|
|
createRagHitLogTable(); |
|
|
|
} |
|
|
|
}); |
|
|
|
safeInit("创建看板快照表 dashboard_snapshot", () -> { |
|
|
|
if (!checkTableExists("dashboard_snapshot")) { |
|
|
|
log.info("创建看板快照表 dashboard_snapshot"); |
|
|
|
createDashboardSnapshotTable(); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
// P1-003: API 开放平台 |
|
|
|
// P1-003: API 开放平台 |
|
|
|
safeInit("创建 API Key 表 api_key", () -> { |
|
|
|
if (!checkTableExists("api_key")) { |
|
|
|
log.info("创建 API Key 表 api_key"); |
|
|
|
createApiKeyTable(); |
|
|
|
} |
|
|
|
}); |
|
|
|
safeInit("创建 Webhook 配置表 webhook_config", () -> { |
|
|
|
if (!checkTableExists("webhook_config")) { |
|
|
|
log.info("创建 Webhook 配置表 webhook_config"); |
|
|
|
createWebhookConfigTable(); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
// P0-001: 混合检索 - 为 vector_store 添加全文检索列 |
|
|
|
safeInit("初始化 vector_store 全文检索", this::initVectorStoreFullTextSearch); |
|
|
|
|
|
|
|
// P0-001: 混合检索 - 为 vector_store 添加全文检索列 |
|
|
|
initVectorStoreFullTextSearch(); |
|
|
|
// 为所有表添加注释(幂等,可重复执行) |
|
|
|
safeInit("应用数据库表注释", this::applyTableComments); |
|
|
|
|
|
|
|
// 为所有表添加注释(幂等,可重复执行) |
|
|
|
applyTableComments(); |
|
|
|
// ==================== 初始化结果验证 ==================== |
|
|
|
verifyInitialization(); |
|
|
|
|
|
|
|
log.info("========== 数据库初始化完成 =========="); |
|
|
|
} |
|
|
|
|
|
|
|
log.info("数据库初始化完成"); |
|
|
|
/** |
|
|
|
* 安全执行初始化步骤,单个步骤失败不影响其他步骤 |
|
|
|
*/ |
|
|
|
private void safeInit(String description, Runnable action) { |
|
|
|
try { |
|
|
|
action.run(); |
|
|
|
} catch (Exception e) { |
|
|
|
log.error("数据库初始化失败", 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", |
|
|
|
"sys_user", "sys_role", "sys_permission", "sys_user_role", |
|
|
|
"rag_hit_log", "dashboard_snapshot", |
|
|
|
"api_key", "webhook_config" |
|
|
|
}; |
|
|
|
|
|
|
|
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("请检查上方错误日志,修复后重启应用"); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -330,6 +385,32 @@ public class DatabaseInitConfig { |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
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'"; |
|
|
|
@ -459,7 +540,7 @@ public class DatabaseInitConfig { |
|
|
|
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_knowledge_document_content_hash ON knowledge_document (content_hash)"); |
|
|
|
} |
|
|
|
} catch (Exception e) { |
|
|
|
log.warn("添加 content_hash 列时出错", e); |
|
|
|
log.error("添加 knowledge_document.content_hash 列失败,请手动执行: ALTER TABLE knowledge_document ADD COLUMN content_hash VARCHAR(64)", e); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -477,7 +558,7 @@ public class DatabaseInitConfig { |
|
|
|
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_knowledge_document_enabled ON knowledge_document (enabled)"); |
|
|
|
} |
|
|
|
} catch (Exception e) { |
|
|
|
log.warn("添加 knowledge_document.enabled 列时出错", e); |
|
|
|
log.error("添加 knowledge_document.enabled 列失败,请手动执行: ALTER TABLE knowledge_document ADD COLUMN enabled BOOLEAN DEFAULT TRUE NOT NULL", e); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -494,7 +575,7 @@ public class DatabaseInitConfig { |
|
|
|
jdbcTemplate.execute("ALTER TABLE knowledge_document ADD COLUMN extra_config JSONB DEFAULT '{}'"); |
|
|
|
} |
|
|
|
} catch (Exception e) { |
|
|
|
log.warn("添加 knowledge_document.extra_config 列时出错", e); |
|
|
|
log.error("添加 knowledge_document.extra_config 列失败,请手动执行: ALTER TABLE knowledge_document ADD COLUMN extra_config JSONB DEFAULT '{}'", e); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -613,7 +694,7 @@ public class DatabaseInitConfig { |
|
|
|
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_faq_category_id ON knowledge_faq (category_id)"); |
|
|
|
} |
|
|
|
} catch (Exception e) { |
|
|
|
log.warn("添加 knowledge_faq.category_id 列时出错", e); |
|
|
|
log.error("添加 knowledge_faq.category_id 列失败,请手动执行: ALTER TABLE knowledge_faq ADD COLUMN category_id BIGINT", e); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -755,7 +836,7 @@ public class DatabaseInitConfig { |
|
|
|
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_chat_message_user_id ON chat_message (user_id)"); |
|
|
|
} |
|
|
|
} catch (Exception e) { |
|
|
|
log.warn("添加 user_id 列时出错", e); |
|
|
|
log.error("添加 chat_message.user_id 列失败,请手动执行: ALTER TABLE chat_message ADD COLUMN user_id BIGINT", e); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -1093,6 +1174,25 @@ public class DatabaseInitConfig { |
|
|
|
// ===== webhook_config ===== |
|
|
|
executeComment("TABLE webhook_config", "Webhook 配置表(事件推送订阅)"); |
|
|
|
|
|
|
|
// ===== 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)"); |
|
|
|
|
|
|
|
|