pg
This commit is contained in:
370
database.js
370
database.js
@@ -93,7 +93,7 @@ function initializeSQLite() {
|
||||
}
|
||||
|
||||
function createSQLiteTables() {
|
||||
// notification_history
|
||||
// Таблица для истории уведомлений
|
||||
db.run(`CREATE TABLE IF NOT EXISTS notification_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
@@ -105,7 +105,34 @@ function createSQLiteTables() {
|
||||
FOREIGN KEY (task_id) REFERENCES tasks (id),
|
||||
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 (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
login TEXT UNIQUE NOT NULL,
|
||||
@@ -137,6 +164,9 @@ function createSQLiteTables() {
|
||||
rework_comment TEXT,
|
||||
closed_at DATETIME,
|
||||
closed_by INTEGER,
|
||||
task_type TEXT DEFAULT 'regular',
|
||||
approver_group_id INTEGER,
|
||||
document_id INTEGER,
|
||||
FOREIGN KEY (created_by) REFERENCES users (id),
|
||||
FOREIGN KEY (deleted_by) REFERENCES users (id),
|
||||
FOREIGN KEY (original_task_id) REFERENCES tasks (id),
|
||||
@@ -189,7 +219,7 @@ function createSQLiteTables() {
|
||||
|
||||
console.log('✅ База данных SQLite инициализирована');
|
||||
|
||||
// Добавляем таблицу для пользовательских настроек
|
||||
// Таблица для пользовательских настроек
|
||||
db.run(`CREATE TABLE IF NOT EXISTS user_settings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER UNIQUE NOT NULL,
|
||||
@@ -310,9 +340,7 @@ function createSQLiteTables() {
|
||||
|
||||
console.log('✅ Таблицы для согласования документов созданы');
|
||||
|
||||
// ===== НОВЫЕ ТАБЛИЦЫ ДЛЯ ГРУПП ПОЛЬЗОВАТЕЛЕЙ =====
|
||||
|
||||
// Таблица для групп пользователей
|
||||
// Таблицы для групп пользователей
|
||||
db.run(`CREATE TABLE IF NOT EXISTS user_groups (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
@@ -323,7 +351,6 @@ function createSQLiteTables() {
|
||||
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,
|
||||
@@ -361,12 +388,77 @@ function createSQLiteTables() {
|
||||
|
||||
console.log('✅ Таблицы для типов документов и документов (расширенные задачи) созданы');
|
||||
|
||||
// Создаем индексы для улучшения производительности
|
||||
createSQLiteIndexes();
|
||||
|
||||
// Запускаем проверку и обновление структуры таблиц
|
||||
setTimeout(() => {
|
||||
checkAndUpdateTableStructure();
|
||||
}, 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() {
|
||||
console.log('🔍 Проверка структуры таблиц...');
|
||||
@@ -403,7 +495,6 @@ function checkAndUpdateTableStructure() {
|
||||
{ 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' }
|
||||
@@ -454,6 +545,34 @@ function checkAndUpdateTableStructure() {
|
||||
{ name: 'created_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: [
|
||||
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
|
||||
{ name: 'name', type: 'TEXT NOT NULL UNIQUE' },
|
||||
@@ -527,49 +646,8 @@ function checkAndUpdateTableStructure() {
|
||||
});
|
||||
});
|
||||
|
||||
// Проверяем индекс для таблицы 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)",
|
||||
"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}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Создаем группу "Секретарь" по умолчанию, если её нет
|
||||
setTimeout(() => {
|
||||
db.get("SELECT id FROM user_groups WHERE name = 'Секретарь'", (err, group) => {
|
||||
if (err || !group) {
|
||||
console.log('🔧 Создаем группу "Секретарь" по умолчанию...');
|
||||
@@ -586,8 +664,6 @@ function checkAndUpdateTableStructure() {
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
}, 1000);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
@@ -728,7 +804,48 @@ async function createPostgresTables() {
|
||||
|
||||
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(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
@@ -815,7 +932,7 @@ async function createPostgresTables() {
|
||||
)
|
||||
`);
|
||||
|
||||
// Добавляем таблицу для пользовательских настроек
|
||||
// Таблица для пользовательских настроек
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS user_settings (
|
||||
id SERIAL PRIMARY KEY,
|
||||
@@ -831,9 +948,7 @@ async function createPostgresTables() {
|
||||
)
|
||||
`);
|
||||
|
||||
// ===== НОВЫЕ ТАБЛИЦЫ ДЛЯ ГРУПП ПОЛЬЗОВАТЕЛЕЙ =====
|
||||
|
||||
// Таблица для групп пользователей
|
||||
// Таблицы для групп пользователей
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS user_groups (
|
||||
id SERIAL PRIMARY KEY,
|
||||
@@ -846,7 +961,6 @@ async function createPostgresTables() {
|
||||
)
|
||||
`);
|
||||
|
||||
// Таблица для связи пользователей с группами
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS user_group_memberships (
|
||||
id SERIAL PRIMARY KEY,
|
||||
@@ -857,7 +971,7 @@ async function createPostgresTables() {
|
||||
)
|
||||
`);
|
||||
|
||||
// Таблицы для документов (существующие)
|
||||
// Таблицы для документов
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS document_types (
|
||||
id SERIAL PRIMARY KEY,
|
||||
@@ -980,40 +1094,7 @@ async function createPostgresTables() {
|
||||
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)',
|
||||
// Индексы для простых документов
|
||||
'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}`);
|
||||
}
|
||||
}
|
||||
await createPostgresIndexes(client);
|
||||
|
||||
client.release();
|
||||
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
|
||||
async function checkPostgresTableStructure() {
|
||||
if (!USE_POSTGRES) return;
|
||||
@@ -1051,32 +1192,31 @@ async function checkPostgresTableStructure() {
|
||||
|
||||
// Определяем ожидаемую структуру таблиц 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' },
|
||||
email_queue: [
|
||||
{ name: 'to_email', type: 'VARCHAR(500) NOT NULL' },
|
||||
{ name: 'subject', type: 'VARCHAR(500) NOT NULL' },
|
||||
{ name: 'html_content', type: 'TEXT NOT NULL' },
|
||||
{ name: 'user_id', type: 'INTEGER REFERENCES users(id)' },
|
||||
{ name: 'task_id', type: 'INTEGER REFERENCES tasks(id)' },
|
||||
{ name: 'notification_type', type: 'VARCHAR(50)' },
|
||||
{ name: 'retry_count', type: 'INTEGER DEFAULT 0' },
|
||||
{ name: 'status', type: 'VARCHAR(50) DEFAULT \'pending\'' },
|
||||
{ name: 'error_message', 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' }
|
||||
email_settings: [
|
||||
{ name: 'setting_key', type: 'VARCHAR(100) PRIMARY KEY' },
|
||||
{ name: 'setting_value', type: 'TEXT' },
|
||||
{ name: 'spam_blocked_until', type: 'TIMESTAMP' },
|
||||
{ name: 'updated_at', type: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP' }
|
||||
],
|
||||
simple_documents: [
|
||||
{ name: 'task_id', type: 'INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE' },
|
||||
{ name: 'document_type_id', type: 'INTEGER REFERENCES simple_document_types(id)' },
|
||||
{ name: 'document_number', type: 'VARCHAR(100)' },
|
||||
{ name: 'document_date', type: 'DATE' },
|
||||
{ name: 'pages_count', type: 'INTEGER' },
|
||||
{ name: 'urgency_level', type: 'VARCHAR(20) CHECK (urgency_level IN (\'normal\', \'urgent\', \'very_urgent\'))' },
|
||||
{ name: 'comment', type: 'TEXT' },
|
||||
{ name: 'refusal_reason', type: 'TEXT' }
|
||||
notification_history: [
|
||||
{ name: 'user_id', type: 'INTEGER NOT NULL REFERENCES users(id)' },
|
||||
{ name: 'task_id', type: 'INTEGER NOT NULL REFERENCES tasks(id)' },
|
||||
{ name: 'notification_type', type: 'VARCHAR(50) NOT NULL' },
|
||||
{ name: 'last_sent_at', type: 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP' },
|
||||
{ name: 'created_at', type: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP' }
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// email-notifications.js
|
||||
const nodemailer = require('nodemailer');
|
||||
const { getDb } = require('./database');
|
||||
|
||||
@@ -7,7 +6,14 @@ class EmailNotifications {
|
||||
this.transporter = null;
|
||||
this.initialized = false;
|
||||
this.notificationCooldown = 12 * 60 * 60 * 1000; // 12 часов в миллисекундах
|
||||
this.spamBlockCooldown = 60 * 60 * 1000; // 60 минут при блокировке спама
|
||||
this.spamBlockedUntil = null;
|
||||
this.isSpamBlocked = false;
|
||||
this.maxRetries = 3;
|
||||
this.init();
|
||||
|
||||
// Запускаем обработку очереди каждые 5 минут
|
||||
setInterval(() => this.processRetryQueue(), 5 * 60 * 1000);
|
||||
}
|
||||
|
||||
async init() {
|
||||
@@ -37,15 +43,381 @@ class EmailNotifications {
|
||||
// Тестируем подключение
|
||||
await this.transporter.verify();
|
||||
this.initialized = true;
|
||||
|
||||
// Восстанавливаем состояние блокировки из БД
|
||||
await this.restoreSpamBlockState();
|
||||
|
||||
console.log('✅ 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) {
|
||||
console.error('❌ Ошибка инициализации Email уведомлений:', error.message);
|
||||
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) {
|
||||
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) {
|
||||
console.warn('⚠️ Email уведомления отключены');
|
||||
return false;
|
||||
|
||||
// Также сохраняем в очередь на случай, если сервис восстановится
|
||||
const queueId = await this.saveToQueue(to, subject, htmlContent, userId, taskId, notificationType);
|
||||
return queueId ? { queued: true, queueId } : false;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -233,15 +618,31 @@ class EmailNotifications {
|
||||
to: to,
|
||||
subject: subject,
|
||||
html: htmlContent,
|
||||
text: htmlContent.replace(/<[^>]*>/g, '') // Конвертируем HTML в текст
|
||||
text: htmlContent.replace(/<[^>]*>/g, '')
|
||||
});
|
||||
|
||||
console.log(`📧 Email отправлен: ${to}, Message ID: ${info.messageId}`);
|
||||
return true;
|
||||
return { sent: true, messageId: info.messageId };
|
||||
|
||||
} catch (error) {
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
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 {
|
||||
console.log(`❌ Не удалось отправить уведомление: пользователь ${userId}, задача ${taskId}, тип ${notificationType}`);
|
||||
console.log(`❌ Не удалось отправить/сохранить уведомление: пользователь ${userId}, задача ${taskId}, тип ${notificationType}`);
|
||||
}
|
||||
|
||||
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() {
|
||||
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
|
||||
const emailNotifications = new EmailNotifications();
|
||||
|
||||
// Экспортируем функцию для очистки старых записей (можно запускать по cron)
|
||||
emailNotifications.clearOldQueueItems = emailNotifications.clearOldQueueItems.bind(emailNotifications);
|
||||
|
||||
module.exports = emailNotifications;
|
||||
Reference in New Issue
Block a user