Files
suanming/server/database/index.cjs
patdelphi 5378b86a9b fix: add database migration for ai_interpretations table structure
- Add automatic migration from analysis_id to reading_id in ai_interpretations table
- Handle existing production databases with old schema structure
- Preserve valid numeric ID records during migration
- Prevent deployment failures due to schema changes
2025-08-23 23:13:21 +08:00

227 lines
7.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
const schema = fs.readFileSync(this.schemaPath, 'utf8');
// 直接执行整个schema文件
this.db.exec(schema);
console.log('数据库结构初始化完成');
} catch (error) {
console.error('数据库结构初始化失败:', error);
throw error;
}
}
// 迁移ai_interpretations表结构
migrateAiInterpretationsTable() {
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) {
// 检查是否有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表结构开始迁移...');
// 创建新表结构
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)
)
`);
// 迁移数据只迁移数字ID的记录
this.db.exec(`
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]*'
`);
// 删除旧表,重命名新表
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)');
console.log('ai_interpretations表迁移完成');
}
}
} catch (error) {
console.error('ai_interpretations表迁移失败:', error);
// 迁移失败不应该阻止应用启动,只记录错误
}
}
// 获取数据库实例
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;
}
}
}
// 创建单例实例
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);
});