mirror of
https://github.com/patdelphi/suanming.git
synced 2026-02-28 05:33:11 +08:00
- 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
227 lines
7.1 KiB
JavaScript
227 lines
7.1 KiB
JavaScript
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);
|
||
}); |