Files
minicrm/database.js
2025-12-21 20:42:12 +05:00

607 lines
23 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 инициализирована');
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' },
{ table: 'tasks', column: 'closed_at', type: 'DATETIME' },
{ table: 'tasks', column: 'closed_by', type: 'INTEGER' },
{ table: 'task_assignments', column: 'rework_comment', type: 'TEXT' }
];
columnsToAdd.forEach(({ table, column, type }) => {
db.all(`PRAGMA table_info(${table})`, (err, rows) => {
if (err) {
console.error(`Ошибка при проверке таблицы ${table}:`, err);
return;
}
const columnExists = rows.some(row => row.name === column);
if (!columnExists) {
db.run(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`, (err) => {
if (err) {
console.error(`Ошибка при добавлении колонки ${column} в таблицу ${table}:`, err);
} else {
console.log(`✅ Добавлена колонка ${column} в таблицу ${table}`);
}
});
} else {
console.log(`✅ Колонка ${column} уже существует в таблице ${table}`);
}
});
});
}
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'
};
// Запускаем инициализацию при экспорте (но она завершится позже)
initializeDatabase().catch(err => {
console.error('❌ Ошибка инициализации базы данных:', err.message);
});