Files
minicrm/database.js
2026-01-27 00:32:06 +05:00

1284 lines
53 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('✅ Таблица для пользовательских настроек инициализирована');
// Таблица для типов документов
db.run(`CREATE TABLE IF NOT EXISTS document_types (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
description TEXT,
template_path TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`);
// Таблица для документов
db.run(`CREATE TABLE IF NOT EXISTS documents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT,
document_type_id INTEGER NOT NULL,
status TEXT DEFAULT 'draft',
created_by INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
approved_at DATETIME,
approved_by INTEGER,
rejected_at DATETIME,
rejected_by INTEGER,
rejection_reason TEXT,
file_path TEXT,
file_name TEXT,
file_size INTEGER,
version INTEGER DEFAULT 1,
parent_document_id INTEGER,
FOREIGN KEY (document_type_id) REFERENCES document_types (id),
FOREIGN KEY (created_by) REFERENCES users (id),
FOREIGN KEY (approved_by) REFERENCES users (id),
FOREIGN KEY (rejected_by) REFERENCES users (id),
FOREIGN KEY (parent_document_id) REFERENCES documents (id)
)`);
// Таблица для этапов согласования
db.run(`CREATE TABLE IF NOT EXISTS approval_stages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
document_type_id INTEGER NOT NULL,
stage_number INTEGER NOT NULL,
stage_name TEXT NOT NULL,
approver_role TEXT,
approver_user_id INTEGER,
is_required BOOLEAN DEFAULT true,
can_edit BOOLEAN DEFAULT false,
can_comment BOOLEAN DEFAULT true,
deadline_days INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(document_type_id, stage_number),
FOREIGN KEY (document_type_id) REFERENCES document_types (id),
FOREIGN KEY (approver_user_id) REFERENCES users (id)
)`);
// Таблица для согласования документов
db.run(`CREATE TABLE IF NOT EXISTS document_approvals (
id INTEGER PRIMARY KEY AUTOINCREMENT,
document_id INTEGER NOT NULL,
stage_id INTEGER NOT NULL,
approver_user_id INTEGER NOT NULL,
status TEXT DEFAULT 'pending',
comments TEXT,
approved_at DATETIME,
rejected_at DATETIME,
deadline DATETIME,
notified_at DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(document_id, stage_id, approver_user_id),
FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE,
FOREIGN KEY (stage_id) REFERENCES approval_stages (id),
FOREIGN KEY (approver_user_id) REFERENCES users (id)
)`);
// Таблица для комментариев к документам
db.run(`CREATE TABLE IF NOT EXISTS document_comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
document_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
comment TEXT NOT NULL,
is_internal BOOLEAN DEFAULT false,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users (id)
)`);
// Таблица для истории изменений документов
db.run(`CREATE TABLE IF NOT EXISTS document_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
document_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
action TEXT NOT NULL,
changes TEXT,
version INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users (id)
)`);
console.log('✅ Таблицы для согласования документов созданы');
// ===== НОВЫЕ ТАБЛИЦЫ ДЛЯ ГРУПП ПОЛЬЗОВАТЕЛЕЙ =====
// Таблица для групп пользователей
db.run(`CREATE TABLE IF NOT EXISTS user_groups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
description TEXT,
color TEXT DEFAULT '#3498db',
can_approve_documents BOOLEAN DEFAULT false,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`);
// Таблица для связи пользователей с группами (многие-ко-многим)
db.run(`CREATE TABLE IF NOT EXISTS user_group_memberships (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
group_id INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, group_id),
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (group_id) REFERENCES user_groups (id) ON DELETE CASCADE
)`);
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' },
// Новые колонки для типа задач
{ name: 'task_type', type: 'TEXT DEFAULT "regular"' },
{ name: 'approver_group_id', type: 'INTEGER' },
{ name: 'document_id', 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' }
],
user_groups: [
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
{ name: 'name', type: 'TEXT NOT NULL UNIQUE' },
{ name: 'description', type: 'TEXT' },
{ name: 'color', type: 'TEXT DEFAULT "#3498db"' },
{ name: 'can_approve_documents', type: 'BOOLEAN DEFAULT false' },
{ name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' },
{ name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }
],
user_group_memberships: [
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
{ name: 'user_id', type: 'INTEGER NOT NULL' },
{ name: 'group_id', type: 'INTEGER NOT NULL' },
{ name: 'created_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 создан');
}
});
}
});
// Создаем индексы для новых таблиц
setTimeout(() => {
const newIndexes = [
"CREATE INDEX IF NOT EXISTS idx_tasks_task_type ON tasks(task_type)",
"CREATE INDEX IF NOT EXISTS idx_tasks_approver_group_id ON tasks(approver_group_id)",
"CREATE INDEX IF NOT EXISTS idx_user_groups_can_approve ON user_groups(can_approve_documents)",
"CREATE INDEX IF NOT EXISTS idx_user_group_memberships_user_id ON user_group_memberships(user_id)",
"CREATE INDEX IF NOT EXISTS idx_user_group_memberships_group_id ON user_group_memberships(group_id)"
];
newIndexes.forEach(indexQuery => {
db.run(indexQuery, (err) => {
if (err) {
console.error(`❌ Ошибка создания индекса: ${err.message}`);
} else {
console.log(`✅ Индекс создан: ${indexQuery}`);
}
});
});
// Создаем группу "Секретарь" по умолчанию, если её нет
db.get("SELECT id FROM user_groups WHERE name = 'Секретарь'", (err, group) => {
if (err || !group) {
console.log('🔧 Создаем группу "Секретарь" по умолчанию...');
db.run(
`INSERT INTO user_groups (name, description, color, can_approve_documents)
VALUES ('Секретарь', 'Группа для согласования документов', '#e74c3c', 1)`,
(insertErr) => {
if (insertErr) {
console.error('❌ Ошибка создания группы "Секретарь":', insertErr.message);
} else {
console.log('✅ Группа "Секретарь" создана по умолчанию');
}
}
);
}
});
}, 1000);
}, 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),
task_type VARCHAR(50) DEFAULT 'regular',
approver_group_id INTEGER,
document_id INTEGER
)
`);
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
)
`);
// ===== НОВЫЕ ТАБЛИЦЫ ДЛЯ ГРУПП ПОЛЬЗОВАТЕЛЕЙ =====
// Таблица для групп пользователей
await client.query(`
CREATE TABLE IF NOT EXISTS user_groups (
id SERIAL PRIMARY KEY,
name VARCHAR(100) UNIQUE NOT NULL,
description TEXT,
color VARCHAR(20) DEFAULT '#3498db',
can_approve_documents BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
// Таблица для связи пользователей с группами
await client.query(`
CREATE TABLE IF NOT EXISTS user_group_memberships (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
group_id INTEGER NOT NULL REFERENCES user_groups(id) ON DELETE CASCADE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, group_id)
)
`);
// Таблицы для документов (существующие)
await client.query(`
CREATE TABLE IF NOT EXISTS document_types (
id SERIAL PRIMARY KEY,
name VARCHAR(100) UNIQUE NOT NULL,
description TEXT,
template_path TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
await client.query(`
CREATE TABLE IF NOT EXISTS documents (
id SERIAL PRIMARY KEY,
title VARCHAR(500) NOT NULL,
description TEXT,
document_type_id INTEGER NOT NULL REFERENCES document_types(id),
status VARCHAR(50) DEFAULT 'draft',
created_by INTEGER NOT NULL REFERENCES users(id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
approved_at TIMESTAMP,
approved_by INTEGER REFERENCES users(id),
rejected_at TIMESTAMP,
rejected_by INTEGER REFERENCES users(id),
rejection_reason TEXT,
file_path TEXT,
file_name TEXT,
file_size BIGINT,
version INTEGER DEFAULT 1,
parent_document_id INTEGER REFERENCES documents(id)
)
`);
await client.query(`
CREATE TABLE IF NOT EXISTS approval_stages (
id SERIAL PRIMARY KEY,
document_type_id INTEGER NOT NULL REFERENCES document_types(id),
stage_number INTEGER NOT NULL,
stage_name VARCHAR(100) NOT NULL,
approver_role VARCHAR(50),
approver_user_id INTEGER REFERENCES users(id),
is_required BOOLEAN DEFAULT true,
can_edit BOOLEAN DEFAULT false,
can_comment BOOLEAN DEFAULT true,
deadline_days INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(document_type_id, stage_number)
)
`);
await client.query(`
CREATE TABLE IF NOT EXISTS document_approvals (
id SERIAL PRIMARY KEY,
document_id INTEGER NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
stage_id INTEGER NOT NULL REFERENCES approval_stages(id),
approver_user_id INTEGER NOT NULL REFERENCES users(id),
status VARCHAR(50) DEFAULT 'pending',
comments TEXT,
approved_at TIMESTAMP,
rejected_at TIMESTAMP,
deadline TIMESTAMP,
notified_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(document_id, stage_id, approver_user_id)
)
`);
await client.query(`
CREATE TABLE IF NOT EXISTS document_comments (
id SERIAL PRIMARY KEY,
document_id INTEGER NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
user_id INTEGER NOT NULL REFERENCES users(id),
comment TEXT NOT NULL,
is_internal BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
await client.query(`
CREATE TABLE IF NOT EXISTS document_history (
id SERIAL PRIMARY KEY,
document_id INTEGER NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
user_id INTEGER NOT NULL REFERENCES users(id),
action VARCHAR(100) NOT NULL,
changes TEXT,
version INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
console.log('✅ Все таблицы PostgreSQL созданы/проверены');
// Создаем индексы
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)',
// Новые индексы
'CREATE INDEX IF NOT EXISTS idx_tasks_task_type ON tasks(task_type)',
'CREATE INDEX IF NOT EXISTS idx_tasks_approver_group_id ON tasks(approver_group_id)',
'CREATE INDEX IF NOT EXISTS idx_user_groups_can_approve ON user_groups(can_approve_documents)',
'CREATE INDEX IF NOT EXISTS idx_user_group_memberships_user_id ON user_group_memberships(user_id)',
'CREATE INDEX IF NOT EXISTS idx_user_group_memberships_group_id ON user_group_memberships(group_id)',
// Индексы для документов
'CREATE INDEX IF NOT EXISTS idx_documents_status ON documents(status)',
'CREATE INDEX IF NOT EXISTS idx_documents_created_by ON documents(created_by)',
'CREATE INDEX IF NOT EXISTS idx_document_approvals_document_id ON document_approvals(document_id)',
'CREATE INDEX IF NOT EXISTS idx_document_approvals_status ON document_approvals(status)'
];
for (const indexQuery of indexes) {
try {
await client.query(indexQuery);
} catch (err) {
console.warn(`⚠️ Не удалось создать индекс: ${err.message}`);
}
}
client.release();
console.log('✅ Таблицы PostgreSQL проверены/созданы');
// Проверяем структуру PostgreSQL таблиц
await checkPostgresTableStructure();
// Создаем группу "Секретарь" по умолчанию
try {
const checkResult = await client.query("SELECT id FROM user_groups WHERE name = 'Секретарь'");
if (checkResult.rows.length === 0) {
await client.query(`
INSERT INTO user_groups (name, description, color, can_approve_documents)
VALUES ('Секретарь', 'Группа для согласования документов', '#e74c3c', true)
`);
console.log('✅ Группа "Секретарь" создана по умолчанию');
}
} catch (error) {
console.warn('⚠️ Не удалось создать группу "Секретарь":', error.message);
}
} 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' }
],
tasks: [
{ name: 'task_type', type: 'VARCHAR(50) DEFAULT "regular"' },
{ name: 'approver_group_id', type: 'INTEGER' },
{ name: 'document_id', type: 'INTEGER' }
]
};
// Проверяем каждую таблицу
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 getUserGroups(userId, callback) {
const query = `
SELECT g.* FROM user_groups g
JOIN user_group_memberships ugm ON g.id = ugm.group_id
WHERE ugm.user_id = ?
ORDER BY g.name
`;
db.all(query, [userId], (err, groups) => {
if (err) {
console.error('❌ Ошибка получения групп пользователя:', err);
callback(err, []);
} else {
callback(null, groups || []);
}
});
}
function getGroupMembers(groupId, callback) {
const query = `
SELECT u.* FROM users u
JOIN user_group_memberships ugm ON u.id = ugm.user_id
WHERE ugm.group_id = ?
ORDER BY u.name
`;
db.all(query, [groupId], (err, members) => {
if (err) {
console.error('❌ Ошибка получения участников группы:', err);
callback(err, []);
} else {
callback(null, members || []);
}
});
}
function getApproverGroups(callback) {
const query = `
SELECT g.*, COUNT(DISTINCT ugm.user_id) as member_count
FROM user_groups g
LEFT JOIN user_group_memberships ugm ON g.id = ugm.group_id
WHERE g.can_approve_documents = true
GROUP BY g.id
ORDER BY g.name
`;
db.all(query, [], (err, groups) => {
if (err) {
console.error('❌ Ошибка получения групп согласующих:', err);
callback(err, []);
} else {
callback(null, groups || []);
}
});
}
function addUserToGroup(userId, groupId, callback) {
const query = `
INSERT INTO user_group_memberships (user_id, group_id)
VALUES (?, ?)
ON CONFLICT (user_id, group_id) DO NOTHING
`;
db.run(query, [userId, groupId], function(err) {
if (err) {
console.error('❌ Ошибка добавления пользователя в группу:', err);
callback(err);
} else {
callback(null, this.changes > 0);
}
});
}
function removeUserFromGroup(userId, groupId, callback) {
const query = `
DELETE FROM user_group_memberships
WHERE user_id = ? AND group_id = ?
`;
db.run(query, [userId, groupId], function(err) {
if (err) {
console.error('❌ Ошибка удаления пользователя из группы:', err);
callback(err);
} else {
callback(null, this.changes > 0);
}
});
}
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, // Экспортируем для ручного запуска
// Новые функции для работы с группами
getUserGroups,
getGroupMembers,
getApproverGroups,
addUserToGroup,
removeUserFromGroup
};
// Запускаем инициализацию при экспорте (но она завершится позже)
initializeDatabase().catch(err => {
console.error('❌ Ошибка инициализации базы данных:', err.message);
});