This commit is contained in:
2026-01-27 22:35:09 +05:00
parent a1c9c833f5
commit eb03509c26
2 changed files with 850 additions and 139 deletions

View File

@@ -93,8 +93,8 @@ function initializeSQLite() {
} }
function createSQLiteTables() { function createSQLiteTables() {
// notification_history // Таблица для истории уведомлений
db.run(`CREATE TABLE IF NOT EXISTS notification_history ( db.run(`CREATE TABLE IF NOT EXISTS notification_history (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
task_id INTEGER NOT NULL, task_id INTEGER NOT NULL,
@@ -105,7 +105,34 @@ function createSQLiteTables() {
FOREIGN KEY (task_id) REFERENCES tasks (id), FOREIGN KEY (task_id) REFERENCES tasks (id),
UNIQUE(user_id, task_id, notification_type) UNIQUE(user_id, task_id, notification_type)
)`); )`);
// SQLite таблицы
// Таблица очереди email
db.run(`CREATE TABLE IF NOT EXISTS email_queue (
id INTEGER PRIMARY KEY AUTOINCREMENT,
to_email TEXT NOT NULL,
subject TEXT NOT NULL,
html_content TEXT NOT NULL,
user_id INTEGER,
task_id INTEGER,
notification_type TEXT,
retry_count INTEGER DEFAULT 0,
status TEXT DEFAULT 'pending',
error_message TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (task_id) REFERENCES tasks(id)
)`);
// Таблица настроек email (для хранения состояния блокировки)
db.run(`CREATE TABLE IF NOT EXISTS email_settings (
setting_key TEXT PRIMARY KEY,
setting_value TEXT,
spam_blocked_until DATETIME,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`);
// Основные таблицы системы
db.run(`CREATE TABLE IF NOT EXISTS users ( db.run(`CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
login TEXT UNIQUE NOT NULL, login TEXT UNIQUE NOT NULL,
@@ -137,6 +164,9 @@ function createSQLiteTables() {
rework_comment TEXT, rework_comment TEXT,
closed_at DATETIME, closed_at DATETIME,
closed_by INTEGER, closed_by INTEGER,
task_type TEXT DEFAULT 'regular',
approver_group_id INTEGER,
document_id INTEGER,
FOREIGN KEY (created_by) REFERENCES users (id), FOREIGN KEY (created_by) REFERENCES users (id),
FOREIGN KEY (deleted_by) REFERENCES users (id), FOREIGN KEY (deleted_by) REFERENCES users (id),
FOREIGN KEY (original_task_id) REFERENCES tasks (id), FOREIGN KEY (original_task_id) REFERENCES tasks (id),
@@ -189,7 +219,7 @@ function createSQLiteTables() {
console.log('✅ База данных SQLite инициализирована'); console.log('✅ База данных SQLite инициализирована');
// Добавляем таблицу для пользовательских настроек // Таблица для пользовательских настроек
db.run(`CREATE TABLE IF NOT EXISTS user_settings ( db.run(`CREATE TABLE IF NOT EXISTS user_settings (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER UNIQUE NOT NULL, user_id INTEGER UNIQUE NOT NULL,
@@ -310,9 +340,7 @@ function createSQLiteTables() {
console.log('✅ Таблицы для согласования документов созданы'); console.log('✅ Таблицы для согласования документов созданы');
// ===== НОВЫЕ ТАБЛИЦЫ ДЛЯ ГРУПП ПОЛЬЗОВАТЕЛЕЙ ===== // Таблицы для групп пользователей
// Таблица для групп пользователей
db.run(`CREATE TABLE IF NOT EXISTS user_groups ( db.run(`CREATE TABLE IF NOT EXISTS user_groups (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE, name TEXT NOT NULL UNIQUE,
@@ -323,7 +351,6 @@ function createSQLiteTables() {
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`); )`);
// Таблица для связи пользователей с группами (многие-ко-многим)
db.run(`CREATE TABLE IF NOT EXISTS user_group_memberships ( db.run(`CREATE TABLE IF NOT EXISTS user_group_memberships (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
@@ -361,12 +388,77 @@ function createSQLiteTables() {
console.log('✅ Таблицы для типов документов и документов (расширенные задачи) созданы'); console.log('✅ Таблицы для типов документов и документов (расширенные задачи) созданы');
// Создаем индексы для улучшения производительности
createSQLiteIndexes();
// Запускаем проверку и обновление структуры таблиц // Запускаем проверку и обновление структуры таблиц
setTimeout(() => { setTimeout(() => {
checkAndUpdateTableStructure(); checkAndUpdateTableStructure();
}, 2000); }, 2000);
} }
function createSQLiteIndexes() {
console.log('🔧 Создаем индексы для SQLite...');
const indexes = [
// Индексы для очереди email
"CREATE INDEX IF NOT EXISTS idx_email_queue_status ON email_queue(status, created_at)",
"CREATE INDEX IF NOT EXISTS idx_email_queue_user_id ON email_queue(user_id)",
"CREATE INDEX IF NOT EXISTS idx_email_queue_task_id ON email_queue(task_id)",
// Индексы для уведомлений
"CREATE INDEX IF NOT EXISTS idx_notification_history_user_id ON notification_history(user_id)",
"CREATE INDEX IF NOT EXISTS idx_notification_history_task_id ON notification_history(task_id)",
"CREATE INDEX IF NOT EXISTS idx_notification_history_type ON notification_history(notification_type)",
"CREATE INDEX IF NOT EXISTS idx_notification_history_sent_at ON notification_history(last_sent_at)",
// Индексы для пользователей
"CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)",
"CREATE INDEX IF NOT EXISTS idx_users_role ON users(role)",
// Индексы для задач
"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_task_type ON tasks(task_type)",
"CREATE INDEX IF NOT EXISTS idx_tasks_due_date ON tasks(due_date)",
"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_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_simple_documents_task_id ON simple_documents(task_id)",
"CREATE INDEX IF NOT EXISTS idx_simple_documents_document_number ON simple_documents(document_number)"
];
indexes.forEach(indexQuery => {
db.run(indexQuery, (err) => {
if (err) {
console.error(`❌ Ошибка создания индекса: ${err.message}`);
} else {
console.log(`✅ Индекс создан: ${indexQuery.split('ON')[1]}`);
}
});
});
}
// Функция для проверки и обновления структуры таблиц // Функция для проверки и обновления структуры таблиц
function checkAndUpdateTableStructure() { function checkAndUpdateTableStructure() {
console.log('🔍 Проверка структуры таблиц...'); console.log('🔍 Проверка структуры таблиц...');
@@ -403,7 +495,6 @@ function checkAndUpdateTableStructure() {
{ name: 'rework_comment', type: 'TEXT' }, { name: 'rework_comment', type: 'TEXT' },
{ name: 'closed_at', type: 'DATETIME' }, { name: 'closed_at', type: 'DATETIME' },
{ name: 'closed_by', type: 'INTEGER' }, { name: 'closed_by', type: 'INTEGER' },
// Новые колонки для типа задач
{ name: 'task_type', type: 'TEXT DEFAULT "regular"' }, { name: 'task_type', type: 'TEXT DEFAULT "regular"' },
{ name: 'approver_group_id', type: 'INTEGER' }, { name: 'approver_group_id', type: 'INTEGER' },
{ name: 'document_id', type: 'INTEGER' } { name: 'document_id', type: 'INTEGER' }
@@ -454,6 +545,34 @@ function checkAndUpdateTableStructure() {
{ name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }, { name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' },
{ name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' } { name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }
], ],
notification_history: [
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
{ name: 'user_id', type: 'INTEGER NOT NULL' },
{ name: 'task_id', type: 'INTEGER NOT NULL' },
{ name: 'notification_type', type: 'TEXT NOT NULL' },
{ name: 'last_sent_at', type: 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP' },
{ name: 'created_at', type: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP' }
],
email_queue: [
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
{ name: 'to_email', type: 'TEXT NOT NULL' },
{ name: 'subject', type: 'TEXT NOT NULL' },
{ name: 'html_content', type: 'TEXT NOT NULL' },
{ name: 'user_id', type: 'INTEGER' },
{ name: 'task_id', type: 'INTEGER' },
{ name: 'notification_type', type: 'TEXT' },
{ name: 'retry_count', type: 'INTEGER DEFAULT 0' },
{ name: 'status', type: 'TEXT DEFAULT "pending"' },
{ name: 'error_message', type: 'TEXT' },
{ name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' },
{ name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }
],
email_settings: [
{ name: 'setting_key', type: 'TEXT PRIMARY KEY' },
{ name: 'setting_value', type: 'TEXT' },
{ name: 'spam_blocked_until', type: 'DATETIME' },
{ name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }
],
user_groups: [ user_groups: [
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' }, { name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
{ name: 'name', type: 'TEXT NOT NULL UNIQUE' }, { name: 'name', type: 'TEXT NOT NULL UNIQUE' },
@@ -527,67 +646,24 @@ function checkAndUpdateTableStructure() {
}); });
}); });
// Проверяем индекс для таблицы user_settings // Создаем группу "Секретарь" по умолчанию, если её нет
setTimeout(() => { setTimeout(() => {
db.get("SELECT name FROM sqlite_master WHERE type='index' AND name='idx_user_settings_user_id'", (err, index) => { db.get("SELECT id FROM user_groups WHERE name = 'Секретарь'", (err, group) => {
if (err) { if (err || !group) {
console.error('❌ Ошибка проверки индекса:', err.message); console.log('🔧 Создаем группу "Секретарь" по умолчанию...');
return; db.run(
} `INSERT INTO user_groups (name, description, color, can_approve_documents)
VALUES ('Секретарь', 'Группа для согласования документов', '#e74c3c', 1)`,
if (!index) { (insertErr) => {
console.log('🔧 Создаем индекс для таблицы user_settings...'); if (insertErr) {
db.run("CREATE INDEX IF NOT EXISTS idx_user_settings_user_id ON user_settings(user_id)", (createErr) => { console.error('❌ Ошибка создания группы "Секретарь":', insertErr.message);
if (createErr) { } else {
console.error('❌ Ошибка создания индекса:', createErr.message); console.log('✅ Группа "Секретарь" создана по умолчанию');
} 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)",
"CREATE INDEX IF NOT EXISTS idx_simple_documents_task_id ON simple_documents(task_id)",
"CREATE INDEX IF NOT EXISTS idx_simple_documents_document_number ON simple_documents(document_number)"
];
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); }, 1000);
} }
@@ -728,7 +804,48 @@ async function createPostgresTables() {
console.log('🔧 Проверяем/создаем таблицы в PostgreSQL...'); console.log('🔧 Проверяем/создаем таблицы в PostgreSQL...');
// Создаем таблицы PostgreSQL // Таблица для истории уведомлений
await client.query(`
CREATE TABLE IF NOT EXISTS notification_history (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id),
task_id INTEGER NOT NULL REFERENCES tasks(id),
notification_type VARCHAR(50) NOT NULL,
last_sent_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, task_id, notification_type)
)
`);
// Таблица очереди email
await client.query(`
CREATE TABLE IF NOT EXISTS email_queue (
id SERIAL PRIMARY KEY,
to_email VARCHAR(500) NOT NULL,
subject VARCHAR(500) NOT NULL,
html_content TEXT NOT NULL,
user_id INTEGER REFERENCES users(id),
task_id INTEGER REFERENCES tasks(id),
notification_type VARCHAR(50),
retry_count INTEGER DEFAULT 0,
status VARCHAR(50) DEFAULT 'pending',
error_message TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
// Таблица настроек email
await client.query(`
CREATE TABLE IF NOT EXISTS email_settings (
setting_key VARCHAR(100) PRIMARY KEY,
setting_value TEXT,
spam_blocked_until TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
// Основные таблицы
await client.query(` await client.query(`
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
@@ -815,7 +932,7 @@ async function createPostgresTables() {
) )
`); `);
// Добавляем таблицу для пользовательских настроек // Таблица для пользовательских настроек
await client.query(` await client.query(`
CREATE TABLE IF NOT EXISTS user_settings ( CREATE TABLE IF NOT EXISTS user_settings (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
@@ -831,9 +948,7 @@ async function createPostgresTables() {
) )
`); `);
// ===== НОВЫЕ ТАБЛИЦЫ ДЛЯ ГРУПП ПОЛЬЗОВАТЕЛЕЙ ===== // Таблицы для групп пользователей
// Таблица для групп пользователей
await client.query(` await client.query(`
CREATE TABLE IF NOT EXISTS user_groups ( CREATE TABLE IF NOT EXISTS user_groups (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
@@ -846,7 +961,6 @@ async function createPostgresTables() {
) )
`); `);
// Таблица для связи пользователей с группами
await client.query(` await client.query(`
CREATE TABLE IF NOT EXISTS user_group_memberships ( CREATE TABLE IF NOT EXISTS user_group_memberships (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
@@ -857,7 +971,7 @@ async function createPostgresTables() {
) )
`); `);
// Таблицы для документов (существующие) // Таблицы для документов
await client.query(` await client.query(`
CREATE TABLE IF NOT EXISTS document_types ( CREATE TABLE IF NOT EXISTS document_types (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
@@ -980,40 +1094,7 @@ async function createPostgresTables() {
console.log('✅ Все таблицы PostgreSQL созданы/проверены'); console.log('✅ Все таблицы PostgreSQL созданы/проверены');
// Создаем индексы // Создаем индексы
const indexes = [ await createPostgresIndexes(client);
'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)',
// Индексы для простых документов
'CREATE INDEX IF NOT EXISTS idx_simple_documents_task_id ON simple_documents(task_id)',
'CREATE INDEX IF NOT EXISTS idx_simple_documents_document_number ON simple_documents(document_number)'
];
for (const indexQuery of indexes) {
try {
await client.query(indexQuery);
} catch (err) {
console.warn(`⚠️ Не удалось создать индекс: ${err.message}`);
}
}
client.release(); client.release();
console.log('✅ Таблицы PostgreSQL проверены/созданы'); console.log('✅ Таблицы PostgreSQL проверены/созданы');
@@ -1040,6 +1121,66 @@ async function createPostgresTables() {
} }
} }
async function createPostgresIndexes(client) {
console.log('🔧 Создаем индексы для PostgreSQL...');
const indexes = [
// Индексы для очереди email
'CREATE INDEX IF NOT EXISTS idx_email_queue_status ON email_queue(status, created_at)',
'CREATE INDEX IF NOT EXISTS idx_email_queue_user_id ON email_queue(user_id)',
'CREATE INDEX IF NOT EXISTS idx_email_queue_task_id ON email_queue(task_id)',
// Индексы для уведомлений
'CREATE INDEX IF NOT EXISTS idx_notification_history_user_id ON notification_history(user_id)',
'CREATE INDEX IF NOT EXISTS idx_notification_history_task_id ON notification_history(task_id)',
'CREATE INDEX IF NOT EXISTS idx_notification_history_type ON notification_history(notification_type)',
'CREATE INDEX IF NOT EXISTS idx_notification_history_sent_at ON notification_history(last_sent_at)',
// Индексы для пользователей
'CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)',
'CREATE INDEX IF NOT EXISTS idx_users_role ON users(role)',
// Индексы для задач
'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_task_type ON tasks(task_type)',
'CREATE INDEX IF NOT EXISTS idx_tasks_due_date ON tasks(due_date)',
'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_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_simple_documents_task_id ON simple_documents(task_id)',
'CREATE INDEX IF NOT EXISTS idx_simple_documents_document_number ON simple_documents(document_number)'
];
for (const indexQuery of indexes) {
try {
await client.query(indexQuery);
} catch (err) {
console.warn(`⚠️ Не удалось создать индекс: ${err.message}`);
}
}
}
// Функция для проверки структуры таблиц PostgreSQL // Функция для проверки структуры таблиц PostgreSQL
async function checkPostgresTableStructure() { async function checkPostgresTableStructure() {
if (!USE_POSTGRES) return; if (!USE_POSTGRES) return;
@@ -1051,32 +1192,31 @@ async function checkPostgresTableStructure() {
// Определяем ожидаемую структуру таблиц PostgreSQL // Определяем ожидаемую структуру таблиц PostgreSQL
const tableSchemas = { const tableSchemas = {
user_settings: [ email_queue: [
{ name: 'id', type: 'SERIAL PRIMARY KEY' }, { name: 'to_email', type: 'VARCHAR(500) NOT NULL' },
{ name: 'user_id', type: 'INTEGER UNIQUE NOT NULL REFERENCES users(id)' }, { name: 'subject', type: 'VARCHAR(500) NOT NULL' },
{ name: 'email_notifications', type: 'BOOLEAN DEFAULT true' }, { name: 'html_content', type: 'TEXT NOT NULL' },
{ name: 'notification_email', type: 'TEXT' }, { name: 'user_id', type: 'INTEGER REFERENCES users(id)' },
{ name: 'telegram_notifications', type: 'BOOLEAN DEFAULT false' }, { name: 'task_id', type: 'INTEGER REFERENCES tasks(id)' },
{ name: 'telegram_chat_id', type: 'TEXT' }, { name: 'notification_type', type: 'VARCHAR(50)' },
{ name: 'vk_notifications', type: 'BOOLEAN DEFAULT false' }, { name: 'retry_count', type: 'INTEGER DEFAULT 0' },
{ name: 'vk_user_id', type: 'TEXT' }, { name: 'status', type: 'VARCHAR(50) DEFAULT \'pending\'' },
{ name: 'error_message', type: 'TEXT' },
{ name: 'created_at', type: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP' }, { name: 'created_at', type: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP' },
{ name: 'updated_at', type: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP' } { name: 'updated_at', type: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP' }
], ],
tasks: [ email_settings: [
{ name: 'task_type', type: 'VARCHAR(50) DEFAULT "regular"' }, { name: 'setting_key', type: 'VARCHAR(100) PRIMARY KEY' },
{ name: 'approver_group_id', type: 'INTEGER' }, { name: 'setting_value', type: 'TEXT' },
{ name: 'document_id', type: 'INTEGER' } { name: 'spam_blocked_until', type: 'TIMESTAMP' },
{ name: 'updated_at', type: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP' }
], ],
simple_documents: [ notification_history: [
{ name: 'task_id', type: 'INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE' }, { name: 'user_id', type: 'INTEGER NOT NULL REFERENCES users(id)' },
{ name: 'document_type_id', type: 'INTEGER REFERENCES simple_document_types(id)' }, { name: 'task_id', type: 'INTEGER NOT NULL REFERENCES tasks(id)' },
{ name: 'document_number', type: 'VARCHAR(100)' }, { name: 'notification_type', type: 'VARCHAR(50) NOT NULL' },
{ name: 'document_date', type: 'DATE' }, { name: 'last_sent_at', type: 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP' },
{ name: 'pages_count', type: 'INTEGER' }, { name: 'created_at', type: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP' }
{ name: 'urgency_level', type: 'VARCHAR(20) CHECK (urgency_level IN (\'normal\', \'urgent\', \'very_urgent\'))' },
{ name: 'comment', type: 'TEXT' },
{ name: 'refusal_reason', type: 'TEXT' }
] ]
}; };

View File

@@ -1,4 +1,3 @@
// email-notifications.js
const nodemailer = require('nodemailer'); const nodemailer = require('nodemailer');
const { getDb } = require('./database'); const { getDb } = require('./database');
@@ -7,7 +6,14 @@ class EmailNotifications {
this.transporter = null; this.transporter = null;
this.initialized = false; this.initialized = false;
this.notificationCooldown = 12 * 60 * 60 * 1000; // 12 часов в миллисекундах this.notificationCooldown = 12 * 60 * 60 * 1000; // 12 часов в миллисекундах
this.spamBlockCooldown = 60 * 60 * 1000; // 60 минут при блокировке спама
this.spamBlockedUntil = null;
this.isSpamBlocked = false;
this.maxRetries = 3;
this.init(); this.init();
// Запускаем обработку очереди каждые 5 минут
setInterval(() => this.processRetryQueue(), 5 * 60 * 1000);
} }
async init() { async init() {
@@ -37,15 +43,381 @@ class EmailNotifications {
// Тестируем подключение // Тестируем подключение
await this.transporter.verify(); await this.transporter.verify();
this.initialized = true; this.initialized = true;
// Восстанавливаем состояние блокировки из БД
await this.restoreSpamBlockState();
console.log('✅ Email уведомления инициализированы'); console.log('✅ Email уведомления инициализированы');
console.log(`📧 Отправитель: ${process.env.YANDEX_EMAIL}`); console.log(`📧 Отправитель: ${process.env.YANDEX_EMAIL}`);
if (this.isSpamBlocked && this.spamBlockedUntil) {
const now = new Date();
if (now < this.spamBlockedUntil) {
const minutesLeft = Math.ceil((this.spamBlockedUntil - now) / (60 * 1000));
console.log(`⏸️ Email отправка заблокирована из-за спама. До разблокировки: ${minutesLeft} минут`);
} else {
this.clearSpamBlock();
}
}
} catch (error) { } catch (error) {
console.error('❌ Ошибка инициализации Email уведомлений:', error.message); console.error('❌ Ошибка инициализации Email уведомлений:', error.message);
this.initialized = false; this.initialized = false;
} }
} }
async restoreSpamBlockState() {
if (!getDb) return;
try {
const db = getDb();
return new Promise((resolve, reject) => {
db.get(
`SELECT spam_blocked_until FROM email_settings WHERE setting_key = 'spam_block'`,
(err, row) => {
if (err) {
console.error('❌ Ошибка получения состояния блокировки:', err);
resolve();
return;
}
if (row && row.spam_blocked_until) {
const blockedUntil = new Date(row.spam_blocked_until);
const now = new Date();
if (now < blockedUntil) {
this.isSpamBlocked = true;
this.spamBlockedUntil = blockedUntil;
console.log(`🔄 Восстановлена блокировка из-за спама до: ${blockedUntil.toLocaleString('ru-RU')}`);
} else {
this.clearSpamBlockFromDB();
}
}
resolve();
}
);
});
} catch (error) {
console.error('❌ Ошибка восстановления состояния блокировки:', error);
}
}
async setSpamBlock() {
this.isSpamBlocked = true;
this.spamBlockedUntil = new Date(Date.now() + this.spamBlockCooldown);
console.log(`🚫 Email отправка заблокирована из-за спама до: ${this.spamBlockedUntil.toLocaleString('ru-RU')}`);
// Сохраняем в БД
await this.saveSpamBlockToDB();
}
async saveSpamBlockToDB() {
if (!getDb) return;
try {
const db = getDb();
return new Promise((resolve, reject) => {
db.run(
`INSERT OR REPLACE INTO email_settings (setting_key, setting_value, spam_blocked_until, updated_at)
VALUES ('spam_block', 'blocked', ?, CURRENT_TIMESTAMP)`,
[this.spamBlockedUntil.toISOString()],
(err) => {
if (err) {
console.error('❌ Ошибка сохранения блокировки в БД:', err);
reject(err);
} else {
console.log('✅ Состояние блокировки сохранено в БД');
resolve();
}
}
);
});
} catch (error) {
console.error('❌ Ошибка сохранения блокировки:', error);
}
}
async clearSpamBlock() {
this.isSpamBlocked = false;
this.spamBlockedUntil = null;
console.log('✅ Блокировка из-за спама снята');
// Очищаем из БД
await this.clearSpamBlockFromDB();
// Запускаем обработку очереди
this.processRetryQueue();
}
async clearSpamBlockFromDB() {
if (!getDb) return;
try {
const db = getDb();
return new Promise((resolve, reject) => {
db.run(
`DELETE FROM email_settings WHERE setting_key = 'spam_block'`,
(err) => {
if (err) {
console.error('❌ Ошибка очистки блокировки из БД:', err);
reject(err);
} else {
console.log('✅ Состояние блокировки очищено из БД');
resolve();
}
}
);
});
} catch (error) {
console.error('❌ Ошибка очистки блокировки:', error);
}
}
isSpamBlockActive() {
if (!this.isSpamBlocked || !this.spamBlockedUntil) {
return false;
}
const now = new Date();
if (now >= this.spamBlockedUntil) {
this.clearSpamBlock();
return false;
}
return true;
}
async saveToQueue(to, subject, htmlContent, userId, taskId, notificationType, retryCount = 0) {
if (!getDb) {
console.warn('⚠️ БД не доступна для сохранения в очередь');
return false;
}
try {
const db = getDb();
return new Promise((resolve, reject) => {
db.run(
`INSERT INTO email_queue
(to_email, subject, html_content, user_id, task_id, notification_type, retry_count, status, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP)`,
[to, subject, htmlContent, userId, taskId, notificationType, retryCount],
function(err) {
if (err) {
console.error('❌ Ошибка сохранения в очередь:', err);
reject(err);
} else {
console.log(`📝 Email сохранен в очередь (ID: ${this.lastID}): ${to}, ${subject}`);
resolve(this.lastID);
}
}
);
});
} catch (error) {
console.error('❌ Ошибка сохранения в очередь:', error);
return false;
}
}
async updateQueueStatus(queueId, status, errorMessage = null, retryCount = null) {
if (!getDb) return false;
try {
const db = getDb();
return new Promise((resolve, reject) => {
let query = `UPDATE email_queue SET status = ?, updated_at = CURRENT_TIMESTAMP`;
const params = [status];
if (errorMessage) {
query += `, error_message = ?`;
params.push(errorMessage);
}
if (retryCount !== null) {
query += `, retry_count = ?`;
params.push(retryCount);
}
query += ` WHERE id = ?`;
params.push(queueId);
db.run(query, params, function(err) {
if (err) {
console.error('❌ Ошибка обновления статуса в очереди:', err);
reject(err);
} else {
console.log(`📝 Статус email в очереди обновлен (ID: ${queueId}): ${status}`);
resolve(this.changes > 0);
}
});
});
} catch (error) {
console.error('❌ Ошибка обновления статуса очереди:', error);
return false;
}
}
async removeFromQueue(queueId) {
if (!getDb) return false;
try {
const db = getDb();
return new Promise((resolve, reject) => {
db.run(
`DELETE FROM email_queue WHERE id = ?`,
[queueId],
function(err) {
if (err) {
console.error('❌ Ошибка удаления из очереди:', err);
reject(err);
} else {
console.log(`🗑️ Email удален из очереди (ID: ${queueId})`);
resolve(this.changes > 0);
}
}
);
});
} catch (error) {
console.error('❌ Ошибка удаления из очереди:', error);
return false;
}
}
async getPendingEmails(limit = 10) {
if (!getDb) return [];
try {
const db = getDb();
return new Promise((resolve, reject) => {
db.all(
`SELECT * FROM email_queue
WHERE status = 'pending'
ORDER BY created_at ASC
LIMIT ?`,
[limit],
(err, rows) => {
if (err) {
console.error('❌ Ошибка получения очереди:', err);
reject(err);
} else {
resolve(rows || []);
}
}
);
});
} catch (error) {
console.error('❌ Ошибка получения очереди:', error);
return [];
}
}
async processRetryQueue() {
if (this.isSpamBlockActive()) {
const minutesLeft = Math.ceil((this.spamBlockedUntil - new Date()) / (60 * 1000));
console.log(`⏸️ Пропуск обработки очереди: блокировка из-за спама (осталось ${minutesLeft} минут)`);
return;
}
if (!this.initialized || !this.transporter) {
console.warn('⚠️ Пропуск обработки очереди: Email не инициализирован');
return;
}
console.log('🔍 Проверка очереди email для повторной отправки...');
try {
const pendingEmails = await this.getPendingEmails(20);
if (pendingEmails.length === 0) {
console.log('📭 Очередь email пуста');
return;
}
console.log(`📧 Найдено ${pendingEmails.length} email в очереди`);
for (const email of pendingEmails) {
try {
// Проверяем, не превышено ли максимальное количество попыток
if (email.retry_count >= this.maxRetries) {
console.log(`⏭️ Пропуск email ${email.id}: превышено максимальное количество попыток (${email.retry_count})`);
await this.updateQueueStatus(email.id, 'failed', 'Превышено максимальное количество попыток отправки');
continue;
}
// Обновляем статус на "в процессе отправки"
await this.updateQueueStatus(email.id, 'sending');
// Отправляем email
const info = await this.transporter.sendMail({
from: `"School CRM" <${process.env.YANDEX_EMAIL}>`,
to: email.to_email,
subject: email.subject,
html: email.html_content,
text: email.html_content.replace(/<[^>]*>/g, '')
});
console.log(`✅ Email отправлен из очереди (ID: ${email.id}): ${email.to_email}, Message ID: ${info.messageId}`);
// Удаляем из очереди при успешной отправке
await this.removeFromQueue(email.id);
// Если есть связанная задача, обновляем историю уведомлений
if (email.user_id && email.task_id && email.notification_type) {
try {
await this.recordNotificationSent(email.user_id, email.task_id, email.notification_type);
console.log(`📝 История уведомлений обновлена для email из очереди (ID: ${email.id})`);
} catch (historyError) {
console.error('❌ Ошибка обновления истории для email из очереди:', historyError);
}
}
// Делаем небольшую паузу между отправками
await new Promise(resolve => setTimeout(resolve, 1000));
} catch (emailError) {
console.error(`❌ Ошибка отправки email из очереди (ID: ${email.id}):`, emailError.message);
// Проверяем, является ли ошибка блокировкой спама
if (emailError.message.includes('554 5.7.1 Message rejected under suspicion of SPAM') ||
emailError.message.includes('suspicion of SPAM') ||
emailError.message.includes('SPAM')) {
console.log('🚫 Обнаружена блокировка из-за спама при обработке очереди');
await this.setSpamBlock();
// Увеличиваем счетчик попыток и возвращаем в очередь
const newRetryCount = (email.retry_count || 0) + 1;
await this.updateQueueStatus(
email.id,
'pending',
`SPAM блокировка: ${emailError.message}`,
newRetryCount
);
break; // Прерываем цикл при блокировке спама
}
// Для других ошибок просто увеличиваем счетчик попыток
const newRetryCount = (email.retry_count || 0) + 1;
await this.updateQueueStatus(
email.id,
'pending',
`Ошибка отправки: ${emailError.message}`,
newRetryCount
);
// Делаем паузу перед следующей попыткой
await new Promise(resolve => setTimeout(resolve, 2000));
}
}
console.log(`✅ Обработка очереди email завершена`);
} catch (error) {
console.error('❌ Ошибка обработки очереди email:', error);
}
}
async canSendNotification(userId, taskId, notificationType) { async canSendNotification(userId, taskId, notificationType) {
if (!getDb) return true; // Если БД не готова, разрешаем отправку if (!getDb) return true; // Если БД не готова, разрешаем отправку
@@ -221,10 +593,23 @@ class EmailNotifications {
}); });
} }
async sendEmailNotification(to, subject, htmlContent) { async sendEmailNotification(to, subject, htmlContent, userId = null, taskId = null, notificationType = null) {
// Проверяем блокировку из-за спама
if (this.isSpamBlockActive()) {
const minutesLeft = Math.ceil((this.spamBlockedUntil - new Date()) / (60 * 1000));
console.log(`⏸️ Email отправка заблокирована из-за спама. Сохраняем в очередь. Осталось: ${minutesLeft} минут`);
// Сохраняем в очередь для последующей отправки
const queueId = await this.saveToQueue(to, subject, htmlContent, userId, taskId, notificationType);
return queueId ? { queued: true, queueId } : false;
}
if (!this.initialized || !this.transporter) { if (!this.initialized || !this.transporter) {
console.warn('⚠️ Email уведомления отключены'); console.warn('⚠️ Email уведомления отключены');
return false;
// Также сохраняем в очередь на случай, если сервис восстановится
const queueId = await this.saveToQueue(to, subject, htmlContent, userId, taskId, notificationType);
return queueId ? { queued: true, queueId } : false;
} }
try { try {
@@ -233,15 +618,31 @@ class EmailNotifications {
to: to, to: to,
subject: subject, subject: subject,
html: htmlContent, html: htmlContent,
text: htmlContent.replace(/<[^>]*>/g, '') // Конвертируем HTML в текст text: htmlContent.replace(/<[^>]*>/g, '')
}); });
console.log(`📧 Email отправлен: ${to}, Message ID: ${info.messageId}`); console.log(`📧 Email отправлен: ${to}, Message ID: ${info.messageId}`);
return true; return { sent: true, messageId: info.messageId };
} catch (error) { } catch (error) {
console.error('❌ Ошибка отправки email:', error.message); console.error('❌ Ошибка отправки email:', error.message);
return false;
// Проверяем, является ли ошибка блокировкой спама
if (error.message.includes('554 5.7.1 Message rejected under suspicion of SPAM') ||
error.message.includes('suspicion of SPAM') ||
error.message.includes('SPAM')) {
console.log('🚫 Обнаружена блокировка из-за спама. Активируем блокировку на 60 минут.');
await this.setSpamBlock();
// Сохраняем email в очередь для повторной отправки после блокировки
const queueId = await this.saveToQueue(to, subject, htmlContent, userId, taskId, notificationType, 1);
return queueId ? { queued: true, queueId, spamBlocked: true } : false;
}
// Для других ошибок также сохраняем в очередь
const queueId = await this.saveToQueue(to, subject, htmlContent, userId, taskId, notificationType, 1);
return queueId ? { queued: true, queueId } : false;
} }
} }
@@ -386,14 +787,20 @@ class EmailNotifications {
htmlContent = this.getDefaultHtml(taskData); htmlContent = this.getDefaultHtml(taskData);
} }
const result = await this.sendEmailNotification(emailTo, subject, htmlContent); const result = await this.sendEmailNotification(emailTo, subject, htmlContent, userId, taskId, notificationType);
// Если уведомление успешно отправлено, записываем в историю // Если уведомление успешно отправлено (не в очередь), записываем в историю
if (result) { if (result && result.sent) {
await this.recordNotificationSent(userId, taskId, notificationType); await this.recordNotificationSent(userId, taskId, notificationType);
console.log(`✅ Уведомление отправлено: пользователь ${userId}, задача ${taskId}, тип ${notificationType}`); console.log(`✅ Уведомление отправлено: пользователь ${userId}, задача ${taskId}, тип ${notificationType}`);
} else if (result && result.queued) {
console.log(`📝 Уведомление сохранено в очередь (ID: ${result.queueId}): пользователь ${userId}, задача ${taskId}, тип ${notificationType}`);
if (result.spamBlocked) {
console.log(`🚫 Отправка заблокирована из-за спама. Email будет отправлен после разблокировки.`);
}
} else { } else {
console.log(`Не удалось отправить уведомление: пользователь ${userId}, задача ${taskId}, тип ${notificationType}`); console.log(`Не удалось отправить/сохранить уведомление: пользователь ${userId}, задача ${taskId}, тип ${notificationType}`);
} }
return result; return result;
@@ -845,11 +1252,175 @@ class EmailNotifications {
}); });
} }
async getEmailQueueStats() {
if (!getDb) return { pending: 0, failed: 0, total: 0 };
return new Promise((resolve, reject) => {
const db = getDb();
const query = `
SELECT
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending,
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed,
COUNT(*) as total
FROM email_queue
`;
db.get(query, [], (err, stats) => {
if (err) {
console.error('❌ Ошибка получения статистики очереди:', err);
resolve({ pending: 0, failed: 0, total: 0 });
} else {
resolve(stats || { pending: 0, failed: 0, total: 0 });
}
});
});
}
async getEmailQueueItems(status = null, limit = 50) {
if (!getDb) return [];
return new Promise((resolve, reject) => {
const db = getDb();
let query = `SELECT * FROM email_queue`;
const params = [];
if (status) {
query += ` WHERE status = ?`;
params.push(status);
}
query += ` ORDER BY created_at DESC LIMIT ?`;
params.push(limit);
db.all(query, params, (err, items) => {
if (err) {
console.error('❌ Ошибка получения элементов очереди:', err);
resolve([]);
} else {
resolve(items || []);
}
});
});
}
async retryFailedEmails() {
if (!getDb) return { retried: 0, total: 0 };
try {
const db = getDb();
// Получаем все проваленные email
const failedEmails = await new Promise((resolve, reject) => {
db.all(
`SELECT * FROM email_queue WHERE status = 'failed' AND retry_count < ?`,
[this.maxRetries],
(err, emails) => {
if (err) reject(err);
else resolve(emails || []);
}
);
});
if (failedEmails.length === 0) {
console.log('📭 Нет проваленных email для повторной отправки');
return { retried: 0, total: 0 };
}
console.log(`🔍 Найдено ${failedEmails.length} проваленных email для повторной отправки`);
let retriedCount = 0;
for (const email of failedEmails) {
try {
// Сбрасываем статус на pending для повторной попытки
await new Promise((resolve, reject) => {
db.run(
`UPDATE email_queue SET status = 'pending', retry_count = retry_count + 1 WHERE id = ?`,
[email.id],
function(err) {
if (err) reject(err);
else {
console.log(`🔄 Email ${email.id} подготовлен для повторной отправки`);
retriedCount++;
resolve();
}
}
);
});
} catch (error) {
console.error(`❌ Ошибка подготовки email ${email.id} для повторной отправки:`, error);
}
}
console.log(`✅ Подготовлено ${retriedCount} email для повторной отправки`);
// Запускаем обработку очереди
this.processRetryQueue();
return { retried: retriedCount, total: failedEmails.length };
} catch (error) {
console.error('❌ Ошибка при попытке повторной отправки email:', error);
return { retried: 0, total: 0 };
}
}
async clearOldQueueItems(days = 30) {
if (!getDb) return { deleted: 0 };
try {
const db = getDb();
const result = await new Promise((resolve, reject) => {
db.run(
`DELETE FROM email_queue
WHERE created_at < datetime('now', '-${days} days')
AND (status = 'sent' OR status = 'failed')`,
function(err) {
if (err) reject(err);
else resolve(this.changes || 0);
}
);
});
console.log(`🗑️ Удалено ${result} старых записей из очереди email (старше ${days} дней)`);
return { deleted: result };
} catch (error) {
console.error('❌ Ошибка очистки старых записей очереди:', error);
return { deleted: 0 };
}
}
isReady() { isReady() {
return this.initialized; return this.initialized;
} }
getSpamBlockStatus() {
if (!this.isSpamBlocked || !this.spamBlockedUntil) {
return { blocked: false, blockedUntil: null, minutesLeft: 0 };
}
const now = new Date();
if (now >= this.spamBlockedUntil) {
this.clearSpamBlock();
return { blocked: false, blockedUntil: null, minutesLeft: 0 };
}
const minutesLeft = Math.ceil((this.spamBlockedUntil - now) / (60 * 1000));
return {
blocked: true,
blockedUntil: this.spamBlockedUntil,
minutesLeft: minutesLeft
};
}
} }
// Singleton // Singleton
const emailNotifications = new EmailNotifications(); const emailNotifications = new EmailNotifications();
// Экспортируем функцию для очистки старых записей (можно запускать по cron)
emailNotifications.clearOldQueueItems = emailNotifications.clearOldQueueItems.bind(emailNotifications);
module.exports = emailNotifications; module.exports = emailNotifications;