diff --git a/src/main/java/com/wok/supportbot/config/DatabaseInitConfig.java b/src/main/java/com/wok/supportbot/config/DatabaseInitConfig.java index daaf3b6..cdfa8fe 100644 --- a/src/main/java/com/wok/supportbot/config/DatabaseInitConfig.java +++ b/src/main/java/com/wok/supportbot/config/DatabaseInitConfig.java @@ -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 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)");