Files
minicrm/database.js
2026-01-26 21:02:00 +05:00

843 lines
34 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 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)) {
fs.mkdirSync(dirPath, { recursive: true });
}
};
createDirIfNotExists(dataDir);
const dbPath = path.join(dataDir, 'school_crm.db');
const uploadsDir = path.join(dataDir, 'uploads');
const tasksDir = path.join(uploadsDir, 'tasks');
const logsDir = path.join(dataDir, 'logs');
createDirIfNotExists(uploadsDir);
createDirIfNotExists(tasksDir);
createDirIfNotExists(logsDir);
// Инициализация базы данных
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,
});
// Тестируем подключение
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,
password TEXT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
role TEXT DEFAULT 'teacher',
auth_type TEXT DEFAULT 'local',
groups TEXT,
description TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_login DATETIME,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`);
db.run(`CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT,
status TEXT DEFAULT 'active',
created_by INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
deleted_at DATETIME,
deleted_by INTEGER,
original_task_id INTEGER,
start_date DATETIME,
due_date DATETIME,
rework_comment TEXT,
closed_at DATETIME,
closed_by INTEGER,
FOREIGN KEY (created_by) REFERENCES users (id),
FOREIGN KEY (deleted_by) REFERENCES users (id),
FOREIGN KEY (original_task_id) REFERENCES tasks (id),
FOREIGN KEY (closed_by) REFERENCES users (id)
)`);
db.run(`CREATE TABLE IF NOT EXISTS task_assignments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
status TEXT DEFAULT 'assigned',
start_date DATETIME,
due_date DATETIME,
rework_comment TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (task_id) REFERENCES tasks (id),
FOREIGN KEY (user_id) REFERENCES users (id)
)`);
db.run(`CREATE TABLE IF NOT EXISTS task_files (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
filename TEXT NOT NULL,
original_name TEXT NOT NULL,
file_path TEXT NOT NULL,
file_size INTEGER NOT NULL,
uploaded_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (task_id) REFERENCES tasks (id),
FOREIGN KEY (user_id) REFERENCES users (id)
)`);
db.run(`CREATE TABLE IF NOT EXISTS activity_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
action TEXT NOT NULL,
details TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (task_id) REFERENCES tasks (id),
FOREIGN KEY (user_id) REFERENCES users (id)
)`);
db.run(`CREATE TABLE IF NOT EXISTS notification_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
notification_key TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`);
console.log('✅ База данных SQLite инициализирована');
// Добавляем таблицу для пользовательских настроек
db.run(`CREATE TABLE IF NOT EXISTS user_settings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER UNIQUE NOT NULL,
email_notifications BOOLEAN DEFAULT true,
notification_email TEXT,
telegram_notifications BOOLEAN DEFAULT false,
telegram_chat_id TEXT,
vk_notifications BOOLEAN DEFAULT false,
vk_user_id TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id)
)`);
console.log('✅ Таблица для пользовательских настроек инициализирована');
// Запускаем проверку и обновление структуры таблиц
setTimeout(() => {
checkAndUpdateTableStructure();
}, 2000);
}
// Функция для проверки и обновления структуры таблиц
function checkAndUpdateTableStructure() {
console.log('🔍 Проверка структуры таблиц...');
// Определяем ожидаемую структуру таблиц
const tableSchemas = {
users: [
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
{ name: 'login', type: 'TEXT UNIQUE NOT NULL' },
{ name: 'password', type: 'TEXT' },
{ name: 'name', type: 'TEXT NOT NULL' },
{ name: 'email', type: 'TEXT UNIQUE NOT NULL' },
{ name: 'role', type: 'TEXT DEFAULT "teacher"' },
{ name: 'auth_type', type: 'TEXT DEFAULT "local"' },
{ name: 'groups', type: 'TEXT' },
{ name: 'description', type: 'TEXT' },
{ name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' },
{ name: 'last_login', type: 'DATETIME' },
{ name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }
],
tasks: [
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
{ name: 'title', type: 'TEXT NOT NULL' },
{ name: 'description', type: 'TEXT' },
{ name: 'status', type: 'TEXT DEFAULT "active"' },
{ name: 'created_by', type: 'INTEGER NOT NULL' },
{ name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' },
{ name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' },
{ name: 'deleted_at', type: 'DATETIME' },
{ name: 'deleted_by', type: 'INTEGER' },
{ name: 'original_task_id', type: 'INTEGER' },
{ name: 'start_date', type: 'DATETIME' },
{ name: 'due_date', type: 'DATETIME' },
{ name: 'rework_comment', type: 'TEXT' },
{ name: 'closed_at', type: 'DATETIME' },
{ name: 'closed_by', type: 'INTEGER' }
],
task_assignments: [
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
{ name: 'task_id', type: 'INTEGER NOT NULL' },
{ name: 'user_id', type: 'INTEGER NOT NULL' },
{ name: 'status', type: 'TEXT DEFAULT "assigned"' },
{ name: 'start_date', type: 'DATETIME' },
{ name: 'due_date', type: 'DATETIME' },
{ name: 'rework_comment', type: 'TEXT' },
{ name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' },
{ name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }
],
task_files: [
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
{ name: 'task_id', type: 'INTEGER NOT NULL' },
{ name: 'user_id', type: 'INTEGER NOT NULL' },
{ name: 'filename', type: 'TEXT NOT NULL' },
{ name: 'original_name', type: 'TEXT NOT NULL' },
{ name: 'file_path', type: 'TEXT NOT NULL' },
{ name: 'file_size', type: 'INTEGER NOT NULL' },
{ name: 'uploaded_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }
],
activity_logs: [
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
{ name: 'task_id', type: 'INTEGER NOT NULL' },
{ name: 'user_id', type: 'INTEGER NOT NULL' },
{ name: 'action', type: 'TEXT NOT NULL' },
{ name: 'details', type: 'TEXT' },
{ name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }
],
notification_logs: [
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
{ name: 'notification_key', type: 'TEXT NOT NULL' },
{ name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }
],
user_settings: [
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
{ name: 'user_id', type: 'INTEGER UNIQUE NOT NULL' },
{ name: 'email_notifications', type: 'BOOLEAN DEFAULT true' },
{ name: 'notification_email', type: 'TEXT' },
{ name: 'telegram_notifications', type: 'BOOLEAN DEFAULT false' },
{ name: 'telegram_chat_id', type: 'TEXT' },
{ name: 'vk_notifications', type: 'BOOLEAN DEFAULT false' },
{ name: 'vk_user_id', type: 'TEXT' },
{ name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' },
{ name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }
]
};
// Проверяем каждую таблицу
Object.entries(tableSchemas).forEach(([tableName, columns]) => {
db.all(`PRAGMA table_info(${tableName})`, (err, existingColumns) => {
if (err) {
console.error(`❌ Ошибка проверки таблицы ${tableName}:`, err.message);
return;
}
if (existingColumns.length === 0) {
console.log(`⚠️ Таблица ${tableName} не существует, создаем...`);
// Таблица будет создана автоматически при следующем запуске
return;
}
// Создаем массив имен существующих колонок
const existingColumnNames = existingColumns.map(col => col.name.toLowerCase());
// Проверяем каждую ожидаемую колонку
columns.forEach(expectedColumn => {
const expectedName = expectedColumn.name.toLowerCase();
if (!existingColumnNames.includes(expectedName)) {
console.log(`🔧 Добавляем колонку ${expectedColumn.name} в таблицу ${tableName}...`);
db.run(
`ALTER TABLE ${tableName} ADD COLUMN ${expectedColumn.name} ${expectedColumn.type}`,
(alterErr) => {
if (alterErr) {
console.error(`❌ Ошибка добавления колонки ${expectedColumn.name}:`, alterErr.message);
} else {
console.log(`✅ Колонка ${expectedColumn.name} добавлена в таблицу ${tableName}`);
}
}
);
}
});
});
});
// Проверяем индекс для таблицы user_settings
setTimeout(() => {
db.get("SELECT name FROM sqlite_master WHERE type='index' AND name='idx_user_settings_user_id'", (err, index) => {
if (err) {
console.error('❌ Ошибка проверки индекса:', err.message);
return;
}
if (!index) {
console.log('🔧 Создаем индекс для таблицы user_settings...');
db.run("CREATE INDEX IF NOT EXISTS idx_user_settings_user_id ON user_settings(user_id)", (createErr) => {
if (createErr) {
console.error('❌ Ошибка создания индекса:', createErr.message);
} else {
console.log('✅ Индекс для user_settings создан');
}
});
}
});
}, 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
)
`);
// Добавляем таблицу для пользовательских настроек
await client.query(`
CREATE TABLE IF NOT EXISTS user_settings (
id SERIAL PRIMARY KEY,
user_id INTEGER UNIQUE NOT NULL REFERENCES users(id),
email_notifications BOOLEAN DEFAULT true,
notification_email TEXT,
telegram_notifications BOOLEAN DEFAULT false,
telegram_chat_id TEXT,
vk_notifications BOOLEAN DEFAULT false,
vk_user_id TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_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)',
'CREATE INDEX IF NOT EXISTS idx_user_settings_user_id ON user_settings(user_id)'
];
for (const indexQuery of indexes) {
try {
await client.query(indexQuery);
} catch (err) {
console.warn(`⚠️ Не удалось создать индекс: ${err.message}`);
}
}
client.release();
console.log('✅ Таблицы PostgreSQL проверены/созданы');
// Проверяем структуру PostgreSQL таблиц
await checkPostgresTableStructure();
} catch (error) {
console.error('❌ Ошибка создания таблиц PostgreSQL:', error.message);
}
}
// Функция для проверки структуры таблиц PostgreSQL
async function checkPostgresTableStructure() {
if (!USE_POSTGRES) return;
try {
const client = await postgresPool.connect();
console.log('🔍 Проверка структуры таблиц PostgreSQL...');
// Определяем ожидаемую структуру таблиц PostgreSQL
const tableSchemas = {
user_settings: [
{ name: 'id', type: 'SERIAL PRIMARY KEY' },
{ name: 'user_id', type: 'INTEGER UNIQUE NOT NULL REFERENCES users(id)' },
{ name: 'email_notifications', type: 'BOOLEAN DEFAULT true' },
{ name: 'notification_email', type: 'TEXT' },
{ name: 'telegram_notifications', type: 'BOOLEAN DEFAULT false' },
{ name: 'telegram_chat_id', type: 'TEXT' },
{ name: 'vk_notifications', type: 'BOOLEAN DEFAULT false' },
{ name: 'vk_user_id', type: 'TEXT' },
{ name: 'created_at', type: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP' },
{ name: 'updated_at', type: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP' }
]
};
// Проверяем каждую таблицу
for (const [tableName, columns] of Object.entries(tableSchemas)) {
try {
// Проверяем существование таблицы
const tableExists = await client.query(
"SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = $1)",
[tableName]
);
if (!tableExists.rows[0].exists) {
console.log(`⚠️ Таблица ${tableName} не существует в PostgreSQL`);
continue;
}
// Получаем существующие колонки
const existingColumns = await client.query(`
SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_name = $1
ORDER BY ordinal_position
`, [tableName]);
const existingColumnNames = existingColumns.rows.map(col => col.column_name.toLowerCase());
// Проверяем каждую ожидаемую колонку
for (const expectedColumn of columns) {
const expectedName = expectedColumn.name.toLowerCase();
if (!existingColumnNames.includes(expectedName)) {
console.log(`🔧 Добавляем колонку ${expectedColumn.name} в таблицу PostgreSQL ${tableName}...`);
try {
await client.query(
`ALTER TABLE ${tableName} ADD COLUMN ${expectedColumn.name} ${expectedColumn.type}`
);
console.log(`✅ Колонка ${expectedColumn.name} добавлена в таблицу PostgreSQL ${tableName}`);
} catch (alterErr) {
console.error(`❌ Ошибка добавления колонки ${expectedColumn.name}:`, alterErr.message);
}
}
}
} catch (error) {
console.error(`❌ Ошибка проверки таблицы PostgreSQL ${tableName}:`, error.message);
}
}
client.release();
console.log('✅ Проверка структуры таблиц PostgreSQL завершена');
} catch (error) {
console.error('❌ Ошибка проверки структуры таблиц PostgreSQL:', error.message);
}
}
function createTaskFolder(taskId) {
const taskFolder = path.join(tasksDir, taskId.toString());
createDirIfNotExists(taskFolder);
return taskFolder;
}
function createUserTaskFolder(taskId, userLogin) {
const taskFolder = path.join(tasksDir, taskId.toString());
const userFolder = path.join(taskFolder, userLogin);
createDirIfNotExists(userFolder);
return userFolder;
}
function saveTaskMetadata(taskId, title, description, createdBy, originalTaskId = null, startDate = null, dueDate = null) {
const taskFolder = createTaskFolder(taskId);
const metadata = {
id: taskId,
title: title,
description: description,
status: 'active',
created_by: createdBy,
original_task_id: originalTaskId,
start_date: startDate,
due_date: dueDate,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
files: []
};
const metadataPath = path.join(taskFolder, 'task.json');
fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
}
function updateTaskMetadata(taskId, updates) {
const metadataPath = path.join(tasksDir, taskId.toString(), 'task.json');
if (fs.existsSync(metadataPath)) {
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
const updatedMetadata = { ...metadata, ...updates, updated_at: new Date().toISOString() };
fs.writeFileSync(metadataPath, JSON.stringify(updatedMetadata, null, 2));
}
}
function logActivity(taskId, userId, action, details = '') {
db.run(
"INSERT INTO activity_logs (task_id, user_id, action, details) VALUES (?, ?, ?, ?)",
[taskId, userId, action, details]
);
const logEntry = `${new Date().toISOString()} - User ${userId}: ${action} - Task ${taskId} - ${details}\n`;
fs.appendFileSync(path.join(logsDir, 'activity.log'), logEntry);
}
function checkTaskAccess(userId, taskId, callback) {
db.get("SELECT role FROM users WHERE id = ?", [userId], (err, user) => {
if (err) {
callback(err, false);
return;
}
if (user && user.role === 'admin') {
callback(null, true);
return;
}
db.get("SELECT status, created_by, closed_at FROM tasks WHERE id = ?", [taskId], (err, task) => {
if (err || !task) {
callback(err, false);
return;
}
if (task.closed_at && task.created_by !== userId && user.role !== 'admin') {
callback(null, false);
return;
}
const query = `
SELECT 1 FROM tasks t
WHERE t.id = ? AND (
t.created_by = ?
OR EXISTS (SELECT 1 FROM task_assignments WHERE task_id = t.id AND user_id = ?)
)
`;
db.get(query, [taskId, userId, userId], (err, row) => {
callback(err, !!row);
});
});
});
}
function checkOverdueTasks() {
const now = new Date().toISOString();
const query = `
SELECT ta.id, ta.task_id, ta.user_id, ta.status, ta.due_date
FROM task_assignments ta
JOIN tasks t ON ta.task_id = t.id
WHERE ta.due_date IS NOT NULL
AND ta.due_date < ?
AND ta.status NOT IN ('completed', 'overdue')
AND t.status = 'active'
AND t.closed_at IS NULL
`;
db.all(query, [now], (err, assignments) => {
if (err) {
console.error('Ошибка при проверке просроченных задач:', err);
return;
}
assignments.forEach(assignment => {
db.run(
"UPDATE task_assignments SET status = 'overdue' WHERE id = ?",
[assignment.id]
);
logActivity(assignment.task_id, assignment.user_id, 'STATUS_CHANGED', 'Задача просрочена');
});
});
}
setInterval(checkOverdueTasks, 60000);
module.exports = {
initializeDatabase, // Экспортируем функцию инициализации
getDb: () => {
if (!isInitialized) {
throw new Error('База данных не инициализирована');
}
return db;
},
isInitialized: () => isInitialized,
logActivity,
createTaskFolder,
createUserTaskFolder,
saveTaskMetadata,
updateTaskMetadata,
checkTaskAccess,
USE_POSTGRES,
getDatabaseType: () => USE_POSTGRES ? 'PostgreSQL' : 'SQLite',
checkAndUpdateTableStructure // Экспортируем для ручного запуска
};
// Запускаем инициализацию при экспорте (но она завершится позже)
initializeDatabase().catch(err => {
console.error('❌ Ошибка инициализации базы данных:', err.message);
});