pg
This commit is contained in:
398
database.js
398
database.js
@@ -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' }
|
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
Reference in New Issue
Block a user