This commit is contained in:
2025-12-21 20:42:12 +05:00
parent 04db1718aa
commit a5c983c6c6
16 changed files with 5880 additions and 2836 deletions

View File

@@ -1,8 +1,15 @@
const sqlite3 = require('sqlite3').verbose();
const { Pool } = require('pg');
const path = require('path');
const fs = require('fs');
require('dotenv').config();
// Определяем, какую базу использовать
const USE_POSTGRES = process.env.POSTGRESQL === 'yes';
let db = null; // Основной объект базы данных
let postgresPool = null; // Пул соединений PostgreSQL
let isInitialized = false; // Флаг инициализации
const dataDir = path.join(__dirname, 'data');
const createDirIfNotExists = (dirPath) => {
if (!fs.existsSync(dirPath)) {
@@ -21,17 +28,72 @@ createDirIfNotExists(uploadsDir);
createDirIfNotExists(tasksDir);
createDirIfNotExists(logsDir);
const db = new sqlite3.Database(dbPath, (err) => {
if (err) {
console.error('Ошибка подключения к БД:', err.message);
} else {
console.log('Подключение к SQLite установлено');
console.log('База данных расположена:', dbPath);
initializeDatabase();
}
});
// Инициализация базы данных
async function initializeDatabase() {
console.log(`🔧 Используется ${USE_POSTGRES ? 'PostgreSQL' : 'SQLite'}`);
if (USE_POSTGRES) {
// Используем PostgreSQL
try {
postgresPool = new Pool({
host: process.env.DB_HOST,
port: process.env.DB_PORT || 5432,
database: process.env.DB_NAME || 'minicrm',
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
max: 10,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 5000,
});
function initializeDatabase() {
// Тестируем подключение
const client = await postgresPool.connect();
await client.query('SELECT 1');
client.release();
console.log('✅ Подключение к PostgreSQL установлено');
// Создаем адаптер для PostgreSQL
db = createPostgresAdapter(postgresPool);
// Проверяем и создаем таблицы
await createPostgresTables();
isInitialized = true;
} catch (error) {
console.error('❌ Ошибка подключения к PostgreSQL:', error.message);
console.log('🔄 Пытаемся использовать SQLite как запасной вариант...');
await initializeSQLite();
}
} else {
// Используем SQLite
await initializeSQLite();
}
return db;
}
function initializeSQLite() {
return new Promise((resolve, reject) => {
db = new sqlite3.Database(dbPath, (err) => {
if (err) {
console.error('❌ Ошибка подключения к SQLite:', err.message);
reject(err);
return;
} else {
console.log('✅ Подключение к SQLite установлено');
console.log('📁 База данных расположена:', dbPath);
createSQLiteTables();
isInitialized = true;
resolve(db);
}
});
});
}
function createSQLiteTables() {
// SQLite таблицы
db.run(`CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
login TEXT UNIQUE NOT NULL,
@@ -113,11 +175,260 @@ function initializeDatabase() {
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`);
console.log('База данных инициализирована в папке data');
console.log('База данных SQLite инициализирована');
setTimeout(addMissingColumns, 1000);
}
function createPostgresAdapter(pool) {
// Адаптер для PostgreSQL с совместимым API
return {
all: (sql, params = [], callback) => {
if (!callback && typeof params === 'function') {
callback = params;
params = [];
}
// Адаптируем SQL для PostgreSQL
const adaptedSql = adaptSQLForPostgres(sql);
pool.query(adaptedSql, params)
.then(result => callback(null, result.rows))
.catch(err => {
console.error('PostgreSQL Error (all):', err.message, 'SQL:', adaptedSql);
callback(err);
});
},
get: (sql, params = [], callback) => {
if (!callback && typeof params === 'function') {
callback = params;
params = [];
}
// Адаптируем SQL для PostgreSQL
const adaptedSql = adaptSQLForPostgres(sql);
pool.query(adaptedSql, params)
.then(result => callback(null, result.rows[0] || null))
.catch(err => {
console.error('PostgreSQL Error (get):', err.message, 'SQL:', adaptedSql);
callback(err);
});
},
run: (sql, params = [], callback) => {
if (!callback && typeof params === 'function') {
callback = params;
params = [];
}
// Адаптируем SQL для PostgreSQL
const adaptedSql = adaptSQLForPostgres(sql);
pool.query(adaptedSql, params)
.then(result => {
if (callback) {
const lastIdQuery = sql.toLowerCase().includes('insert into') ?
"SELECT lastval() as last_id" : "SELECT 0 as last_id";
if (sql.toLowerCase().includes('insert into')) {
pool.query("SELECT lastval() as last_id", [])
.then(lastIdResult => {
callback(null, {
lastID: lastIdResult.rows[0]?.last_id || null,
changes: result.rowCount || 0
});
})
.catch(err => callback(err));
} else {
callback(null, {
lastID: null,
changes: result.rowCount || 0
});
}
}
})
.catch(err => {
console.error('PostgreSQL Error (run):', err.message, 'SQL:', adaptedSql);
if (callback) callback(err);
});
},
// Для транзакций - эмуляция
serialize: (callback) => {
// В PostgreSQL транзакции обрабатываются по-другому
// Здесь просто выполняем колбэк
try {
callback();
} catch (error) {
console.error('Error in serialize:', error);
}
},
// Закрытие соединения
close: (callback) => {
pool.end()
.then(() => {
if (callback) callback(null);
})
.catch(err => {
if (callback) callback(err);
});
},
// Дополнительные методы
exec: (sql, callback) => {
pool.query(sql)
.then(() => {
if (callback) callback(null);
})
.catch(err => {
if (callback) callback(err);
});
}
};
}
function adaptSQLForPostgres(sql) {
// Адаптируем SQL запросы для PostgreSQL
let adaptedSql = sql;
// Заменяем SQLite-специфичные синтаксисы
adaptedSql = adaptedSql.replace(/AUTOINCREMENT/gi, 'SERIAL');
adaptedSql = adaptedSql.replace(/DATETIME/gi, 'TIMESTAMP');
adaptedSql = adaptedSql.replace(/INTEGER PRIMARY KEY/gi, 'SERIAL PRIMARY KEY');
adaptedSql = adaptedSql.replace(/datetime\('now'\)/gi, 'CURRENT_TIMESTAMP');
adaptedSql = adaptedSql.replace(/CURRENT_TIMESTAMP/gi, 'CURRENT_TIMESTAMP');
// Исправляем INSERT с возвратом ID
if (adaptedSql.includes('INSERT INTO') && adaptedSql.includes('RETURNING id')) {
adaptedSql = adaptedSql.replace('RETURNING id', 'RETURNING id');
}
return adaptedSql;
}
async function createPostgresTables() {
if (!USE_POSTGRES) return;
try {
const client = await postgresPool.connect();
console.log('🔧 Проверяем/создаем таблицы в PostgreSQL...');
// Создаем таблицы PostgreSQL
await client.query(`
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
login VARCHAR(100) UNIQUE NOT NULL,
password TEXT,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
role VARCHAR(50) DEFAULT 'teacher',
auth_type VARCHAR(50) DEFAULT 'local',
groups TEXT DEFAULT '[]',
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_login TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
await client.query(`
CREATE TABLE IF NOT EXISTS tasks (
id SERIAL PRIMARY KEY,
title VARCHAR(500) NOT NULL,
description TEXT,
status VARCHAR(50) DEFAULT 'active',
created_by INTEGER NOT NULL REFERENCES users(id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP,
deleted_by INTEGER REFERENCES users(id),
original_task_id INTEGER REFERENCES tasks(id),
start_date TIMESTAMP,
due_date TIMESTAMP,
rework_comment TEXT,
closed_at TIMESTAMP,
closed_by INTEGER REFERENCES users(id)
)
`);
await client.query(`
CREATE TABLE IF NOT EXISTS task_assignments (
id SERIAL PRIMARY KEY,
task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
user_id INTEGER NOT NULL REFERENCES users(id),
status VARCHAR(50) DEFAULT 'assigned',
start_date TIMESTAMP,
due_date TIMESTAMP,
rework_comment TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
await client.query(`
CREATE TABLE IF NOT EXISTS task_files (
id SERIAL PRIMARY KEY,
task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
user_id INTEGER NOT NULL REFERENCES users(id),
filename VARCHAR(255) NOT NULL,
original_name VARCHAR(500) NOT NULL,
file_path TEXT NOT NULL,
file_size BIGINT NOT NULL,
uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
await client.query(`
CREATE TABLE IF NOT EXISTS activity_logs (
id SERIAL PRIMARY KEY,
task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
user_id INTEGER NOT NULL REFERENCES users(id),
action VARCHAR(100) NOT NULL,
details TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
await client.query(`
CREATE TABLE IF NOT EXISTS notification_logs (
id SERIAL PRIMARY KEY,
notification_key VARCHAR(500) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
// Создаем индексы
const indexes = [
'CREATE INDEX IF NOT EXISTS idx_tasks_created_by ON tasks(created_by)',
'CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status)',
'CREATE INDEX IF NOT EXISTS idx_tasks_closed_at ON tasks(closed_at)',
'CREATE INDEX IF NOT EXISTS idx_task_assignments_task_id ON task_assignments(task_id)',
'CREATE INDEX IF NOT EXISTS idx_task_assignments_user_id ON task_assignments(user_id)',
'CREATE INDEX IF NOT EXISTS idx_task_assignments_status ON task_assignments(status)',
'CREATE INDEX IF NOT EXISTS idx_task_files_task_id ON task_files(task_id)',
'CREATE INDEX IF NOT EXISTS idx_activity_logs_task_id ON activity_logs(task_id)',
'CREATE INDEX IF NOT EXISTS idx_activity_logs_created_at ON activity_logs(created_at)'
];
for (const indexQuery of indexes) {
try {
await client.query(indexQuery);
} catch (err) {
console.warn(`⚠️ Не удалось создать индекс: ${err.message}`);
}
}
client.release();
console.log('✅ Таблицы PostgreSQL проверены/созданы');
} catch (error) {
console.error('❌ Ошибка создания таблиц PostgreSQL:', error.message);
}
}
function addMissingColumns() {
const columnsToAdd = [
{ table: 'tasks', column: 'rework_comment', type: 'TEXT' },
@@ -272,11 +583,25 @@ function checkOverdueTasks() {
setInterval(checkOverdueTasks, 60000);
module.exports = {
db,
initializeDatabase, // Экспортируем функцию инициализации
getDb: () => {
if (!isInitialized) {
throw new Error('База данных не инициализирована');
}
return db;
},
isInitialized: () => isInitialized,
logActivity,
createTaskFolder,
createUserTaskFolder,
saveTaskMetadata,
updateTaskMetadata,
checkTaskAccess
};
checkTaskAccess,
USE_POSTGRES,
getDatabaseType: () => USE_POSTGRES ? 'PostgreSQL' : 'SQLite'
};
// Запускаем инициализацию при экспорте (но она завершится позже)
initializeDatabase().catch(err => {
console.error('❌ Ошибка инициализации базы данных:', err.message);
});