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
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);
|
|
}
|
|
}
|
|
|
|
}
|