const Database = require('better-sqlite3'); const path = require('path'); const fs = require('fs'); class DatabaseManager { constructor() { this.db = null; // 检测Koyeb环境并使用正确的挂载路径 const isKoyeb = process.env.KOYEB_APP_NAME || process.env.KOYEB_SERVICE_NAME || fs.existsSync('/workspace/data'); if (isKoyeb) { // Koyeb环境:Volume挂载到/workspace/data this.dbPath = '/workspace/data/numerology.db'; } else if (process.env.NODE_ENV === 'production') { // 其他生产环境:使用/app/data this.dbPath = '/app/data/numerology.db'; } else { // 开发环境:使用本地路径 this.dbPath = path.join(__dirname, '../../numerology.db'); } this.schemaPath = path.join(__dirname, 'schema.sql'); // 输出数据库配置信息 console.log(`🗄️ 数据库路径: ${this.dbPath}`); console.log(`🌍 运行环境: ${process.env.NODE_ENV || 'development'}`); console.log(`📊 数据库文件: ${path.basename(this.dbPath)}`); console.log(`🏢 Koyeb环境: ${isKoyeb ? 'Yes' : 'No'}`); console.log(`📁 工作目录: ${process.cwd()}`); } // 初始化数据库连接 init() { try { // 确保数据库目录存在 const dbDir = path.dirname(this.dbPath); if (!fs.existsSync(dbDir)) { fs.mkdirSync(dbDir, { recursive: true }); console.log(`📁 创建数据库目录: ${dbDir}`); } else { console.log(`📁 数据库目录已存在: ${dbDir}`); } // 创建或连接到SQLite数据库 this.db = new Database(this.dbPath); // 启用外键约束 this.db.pragma('foreign_keys = ON'); // 设置WAL模式以提高并发性能 this.db.pragma('journal_mode = WAL'); // 初始化数据库结构 this.initializeSchema(); console.log('数据库初始化成功'); return this.db; } catch (error) { console.error('数据库初始化失败:', error); throw error; } } // 初始化数据库结构 initializeSchema() { try { // 首先检查是否需要迁移ai_interpretations表 this.migrateAiInterpretationsTable(); // 检查并迁移numerology_readings表以支持qimen类型 this.migrateQimenSupport(); const schema = fs.readFileSync(this.schemaPath, 'utf8'); // 直接执行整个schema文件 this.db.exec(schema); console.log('数据库结构初始化完成'); } catch (error) { console.error('数据库结构初始化失败:', error); throw error; } } /** * 迁移ai_interpretations表结构 * 将旧的analysis_id字段迁移为reading_id字段,建立正确的外键关系 */ migrateAiInterpretationsTable() { let transaction; try { // 检查ai_interpretations表是否存在且使用旧的analysis_id字段 const tableInfo = this.db.prepare(` SELECT name FROM sqlite_master WHERE type='table' AND name='ai_interpretations' `).get(); if (!tableInfo) { console.log('ai_interpretations表不存在,跳过迁移'); return; } // 检查是否有analysis_id字段(旧结构) const columnInfo = this.db.prepare(` PRAGMA table_info(ai_interpretations) `).all(); const hasAnalysisId = columnInfo.some(col => col.name === 'analysis_id'); const hasReadingId = columnInfo.some(col => col.name === 'reading_id'); if (!hasAnalysisId || hasReadingId) { console.log('ai_interpretations表结构已是最新,跳过迁移'); return; } console.log('检测到旧的ai_interpretations表结构,开始迁移...'); // 开始事务 transaction = this.db.transaction(() => { // 创建新表结构 this.db.exec(` CREATE TABLE ai_interpretations_new ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, reading_id INTEGER NOT NULL, content TEXT NOT NULL, model TEXT, tokens_used INTEGER, success BOOLEAN DEFAULT 1, error_message TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (reading_id) REFERENCES numerology_readings(id) ON DELETE CASCADE, UNIQUE(reading_id) ) `); // 检查有多少条记录需要迁移 const countStmt = this.db.prepare(` SELECT COUNT(*) as count FROM ai_interpretations WHERE analysis_id GLOB '[0-9]*' `); const { count } = countStmt.get(); console.log(`准备迁移 ${count} 条有效记录`); // 迁移数据(只迁移数字ID的记录) const migrateStmt = this.db.prepare(` INSERT INTO ai_interpretations_new (user_id, reading_id, content, model, tokens_used, success, error_message, created_at, updated_at) SELECT user_id, CAST(analysis_id AS INTEGER), content, model, tokens_used, success, error_message, created_at, updated_at FROM ai_interpretations WHERE analysis_id GLOB '[0-9]*' `); const migrateResult = migrateStmt.run(); console.log(`成功迁移 ${migrateResult.changes} 条记录`); // 删除旧表,重命名新表 this.db.exec('DROP TABLE ai_interpretations'); this.db.exec('ALTER TABLE ai_interpretations_new RENAME TO ai_interpretations'); // 重新创建索引 this.db.exec('CREATE INDEX IF NOT EXISTS idx_ai_interpretations_user_id ON ai_interpretations(user_id)'); this.db.exec('CREATE INDEX IF NOT EXISTS idx_ai_interpretations_reading_id ON ai_interpretations(reading_id)'); this.db.exec('CREATE INDEX IF NOT EXISTS idx_ai_interpretations_created_at ON ai_interpretations(created_at DESC)'); }); // 执行事务 transaction(); console.log('ai_interpretations表迁移完成'); } catch (error) { console.error('ai_interpretations表迁移失败:', error); console.error('错误详情:', error.message); // 如果事务失败,尝试回滚(SQLite会自动回滚失败的事务) try { // 检查是否存在临时表,如果存在则清理 const tempTableCheck = this.db.prepare(` SELECT name FROM sqlite_master WHERE type='table' AND name='ai_interpretations_new' `).get(); if (tempTableCheck) { this.db.exec('DROP TABLE ai_interpretations_new'); console.log('已清理临时表'); } } catch (cleanupError) { console.error('清理临时表失败:', cleanupError); } // 迁移失败不应该阻止应用启动,只记录错误 } } // 获取数据库实例 getDatabase() { if (!this.db) { this.init(); } return this.db; } // 关闭数据库连接 close() { if (this.db) { this.db.close(); this.db = null; console.log('数据库连接已关闭'); } } // 执行事务 transaction(callback) { const db = this.getDatabase(); const transaction = db.transaction(callback); return transaction; } // 备份数据库 backup(backupPath) { try { const db = this.getDatabase(); db.backup(backupPath); console.log(`数据库备份成功: ${backupPath}`); } catch (error) { console.error('数据库备份失败:', error); throw error; } } // 清理过期会话 cleanupExpiredSessions() { try { const db = this.getDatabase(); const stmt = db.prepare('DELETE FROM user_sessions WHERE expires_at < ?'); const result = stmt.run(new Date().toISOString()); console.log(`清理了 ${result.changes} 个过期会话`); return result.changes; } catch (error) { console.error('清理过期会话失败:', error); throw error; } } /** * 迁移numerology_readings表以支持qimen类型 * 检查现有约束并在需要时更新 */ migrateQimenSupport() { try { // 检查numerology_readings表是否存在 const tableExists = this.db.prepare(` SELECT name FROM sqlite_master WHERE type='table' AND name='numerology_readings' `).get(); if (!tableExists) { console.log('numerology_readings表不存在,跳过qimen迁移'); return; } // 检查是否已经支持qimen类型 try { // 尝试插入一个qimen类型的测试记录来检查约束 const testStmt = this.db.prepare(` INSERT INTO numerology_readings (user_id, reading_type, name) VALUES (?, ?, ?) `); const testResult = testStmt.run(1, 'qimen', 'test_qimen_support'); // 如果插入成功,说明已经支持qimen,删除测试记录 const deleteStmt = this.db.prepare('DELETE FROM numerology_readings WHERE id = ?'); deleteStmt.run(testResult.lastInsertRowid); console.log('✅ numerology_readings表已支持qimen类型'); return; } catch (error) { if (error.code === 'SQLITE_CONSTRAINT_CHECK') { console.log('🔄 检测到需要更新numerology_readings表约束以支持qimen类型'); // 执行表重建以更新约束 const transaction = this.db.transaction(() => { // 创建临时表,包含新的CHECK约束 this.db.exec(` CREATE TABLE numerology_readings_temp ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, reading_type TEXT NOT NULL CHECK (reading_type IN ('bazi', 'ziwei', 'yijing', 'wuxing', 'qimen')), name TEXT, birth_date TEXT, birth_time TEXT, birth_place TEXT, gender TEXT, input_data TEXT, results TEXT, analysis TEXT, status TEXT DEFAULT 'completed' CHECK (status IN ('pending', 'processing', 'completed', 'failed')), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); `); // 复制现有数据到临时表 this.db.exec(` INSERT INTO numerology_readings_temp SELECT id, user_id, reading_type, name, birth_date, birth_time, birth_place, gender, input_data, results, analysis, status, created_at, updated_at FROM numerology_readings; `); // 删除原表 this.db.exec('DROP TABLE numerology_readings;'); // 重命名临时表为原表名 this.db.exec('ALTER TABLE numerology_readings_temp RENAME TO numerology_readings;'); }); transaction(); console.log('✅ numerology_readings表约束更新完成,现已支持qimen类型'); } else { throw error; } } } catch (error) { console.error('❌ qimen支持迁移失败:', error); throw error; } } } // 创建单例实例 const dbManager = new DatabaseManager(); // 导出数据库管理器和便捷方法 module.exports = { dbManager, getDB: () => dbManager.getDatabase(), closeDB: () => dbManager.close(), transaction: (callback) => dbManager.transaction(callback), backup: (path) => dbManager.backup(path), cleanupSessions: () => dbManager.cleanupExpiredSessions() }; // 进程退出时自动关闭数据库 process.on('exit', () => { dbManager.close(); }); process.on('SIGINT', () => { dbManager.close(); process.exit(0); }); process.on('SIGTERM', () => { dbManager.close(); process.exit(0); });