From 73ace950fa204d036b048ac5ae63b8acd5aa7e74 Mon Sep 17 00:00:00 2001 From: kalugin66 Date: Mon, 6 Apr 2026 23:39:27 +0500 Subject: [PATCH] =?UTF-8?q?=D1=87=D0=B0=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cron-jobs.js | 24 ++++++- database.js | 3 +- notifications.js | 162 ++++++++++++++++++++++++++++++++++++++++++++++- server.js | 55 ++++++++++++++-- 4 files changed, 235 insertions(+), 9 deletions(-) diff --git a/cron-jobs.js b/cron-jobs.js index 79580e6..8c65dc6 100644 --- a/cron-jobs.js +++ b/cron-jobs.js @@ -1,5 +1,6 @@ // cron-jobs.js const { logActivity } = require('./database'); +const { sendChatSummaryNotifications } = require('./notifications'); /** * Проверяет задачи с типом document_approval, у которых указан номер документа, @@ -87,6 +88,27 @@ function checkDocumentsForCompletion(db) { }); } +/** + * Запускает cron-задачу для отправки сводок о новых сообщениях в чате. + * Отправка происходит раз в час. + */ +function startChatNotificationsCron() { + console.log('🕐 [CRON] Запущен планировщик уведомлений чата (каждый час)'); + + // Первый запуск через 5 секунд после старта сервера, затем каждый час + setTimeout(async () => { + console.log('📢 [CRON] Первый запуск отправки сводок чата...'); + await sendChatSummaryNotifications(); + + // Запускаем интервал + setInterval(async () => { + console.log('📢 [CRON] Плановый запуск отправки сводок чата...'); + await sendChatSummaryNotifications(); + }, 60 * 60 * 1000); // 1 час + }, 5000); // 5 секунд +} + module.exports = { - checkDocumentsForCompletion + checkDocumentsForCompletion, + startChatNotificationsCron }; \ No newline at end of file diff --git a/database.js b/database.js index 1fbc8e9..3ec4bc8 100644 --- a/database.js +++ b/database.js @@ -774,7 +774,8 @@ function checkAndUpdateTableStructure() { { name: 'vk_notifications', type: 'BOOLEAN DEFAULT false' }, { name: 'vk_user_id', type: 'TEXT' }, { name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }, - { name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' } + { name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }, + { name: 'last_chat_notification_sent_at', type: 'TIMESTAMP' } ], notification_history: [ { name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' }, diff --git a/notifications.js b/notifications.js index b88e3fc..386bb76 100644 --- a/notifications.js +++ b/notifications.js @@ -232,11 +232,171 @@ function getStatusText(status) { return statusMap[status] || status; } +/** + * Отправляет сводку о непрочитанных сообщениях в чатах всем пользователям, + * у которых есть такие сообщения (раз в час). + */ +async function sendChatSummaryNotifications() { + const db = getDb(); + if (!db) { + console.error('❌ База данных не доступна для отправки сводки чата'); + return; + } + + // Получаем всех пользователей, у которых включены email уведомления + const users = await new Promise((resolve, reject) => { + db.all(` + SELECT u.id, u.name, u.email, u.login, + COALESCE(us.email_notifications, 1) as email_notifications, + us.notification_email, + us.last_chat_notification_sent_at + FROM users u + LEFT JOIN user_settings us ON u.id = us.user_id + WHERE u.email IS NOT NULL AND u.email != '' + `, (err, rows) => { + if (err) reject(err); + else resolve(rows || []); + }); + }); + + for (const user of users) { + // Проверяем настройки уведомлений + if (!user.email_notifications) continue; + const emailTo = user.notification_email || user.email; + if (!emailTo) continue; + + // Время последней отправки уведомления (или NULL – тогда берём текущее, но при логине уже должно быть установлено) + let lastSent = user.last_chat_notification_sent_at; + if (!lastSent) { + // Если нет метки – устанавливаем сейчас и пропускаем (при первом входе всё равно сбросится) + await new Promise((resolve) => { + db.run( + `UPDATE user_settings SET last_chat_notification_sent_at = CURRENT_TIMESTAMP WHERE user_id = ?`, + [user.id], + resolve + ); + }); + continue; + } + + // Получаем задачи с количеством новых сообщений после lastSent + const tasks = await new Promise((resolve, reject) => { + db.all(` + SELECT + t.id, + t.title, + COUNT(m.id) as new_messages_count + FROM tasks t + LEFT JOIN task_chat_messages m ON t.id = m.task_id + AND m.created_at > ? + AND m.is_deleted = 0 + WHERE (t.created_by = ? OR EXISTS ( + SELECT 1 FROM task_assignments ta + WHERE ta.task_id = t.id AND ta.user_id = ? + )) + GROUP BY t.id + HAVING new_messages_count > 0 + `, [lastSent, user.id, user.id], (err, rows) => { + if (err) reject(err); + else resolve(rows || []); + }); + }); + + if (tasks.length === 0) continue; + + // Формируем HTML‑письмо со списком задач + const appUrl = process.env.APP_URL || 'http://localhost:3000'; + let tasksHtml = ''; + for (const task of tasks) { + const taskUrl = `${appUrl}/task?id=${task.id}`; + tasksHtml += ` +
  • + ${escapeHtml(task.title)}
    + Новых сообщений: ${task.new_messages_count} +
  • + `; + } + + const subject = `📬 Новые сообщения в чатах задач (${tasks.length} задач)`; + const htmlContent = ` + + + + + + +
    +
    +

    💬 Новые сообщения в чатах

    +
    +
    +

    Здравствуйте, ${escapeHtml(user.name)}!

    +

    У вас есть непрочитанные сообщения в следующих задачах:

    +
      ${tasksHtml}
    +

    Вы получили это письмо, потому что подписаны на уведомления о чатах.
    + Уведомления приходят раз в час. Чтобы отключить их, измените настройки в личном кабинете.

    +
    + +
    + + + `; + + // Отправляем email + const result = await emailNotifications.sendEmailNotification( + emailTo, + subject, + htmlContent, + user.id, + null, + 'chat_summary' + ); + + if (result.sent) { + // Обновляем время последней отправки + await new Promise((resolve) => { + db.run( + `UPDATE user_settings SET last_chat_notification_sent_at = CURRENT_TIMESTAMP WHERE user_id = ?`, + [user.id], + resolve + ); + }); + console.log(`✅ Отправлена сводка чата для ${user.login} (${tasks.length} задач)`); + } else { + console.error(`❌ Не удалось отправить сводку чата для ${user.login}: ${result.error}`); + // Не обновляем last_sent – в следующий раз попробуем снова + } + } +} + +/** + * Вспомогательная функция для экранирования HTML + */ +function escapeHtml(str) { + if (!str) return ''; + return str.replace(/[&<>]/g, function(m) { + if (m === '&') return '&'; + if (m === '<') return '<'; + if (m === '>') return '>'; + return m; + }); +} + // Экспортируем функции module.exports = { sendTaskNotifications, checkUpcomingDeadlines, sendDeadlineNotification, getStatusText, - emailNotifications // Экспортируем для доступа к методам + emailNotifications, + sendChatSummaryNotifications }; \ No newline at end of file diff --git a/server.js b/server.js index c257043..3b37bff 100644 --- a/server.js +++ b/server.js @@ -234,7 +234,47 @@ app.post('/api/login', async (req, res) => { }; req.session.user = sessionUser; - + + // ✅ Обновляем last_chat_notification_sent_at в user_settings + const db = getDb(); + if (db) { + // Проверяем, есть ли запись в user_settings + db.get("SELECT id FROM user_settings WHERE user_id = ?", [user.id], (err, settings) => { + if (err) { + console.error('❌ Ошибка проверки user_settings:', err); + } else if (settings) { + // Обновляем существующую запись + db.run( + `UPDATE user_settings + SET last_chat_notification_sent_at = CURRENT_TIMESTAMP, + updated_at = CURRENT_TIMESTAMP + WHERE user_id = ?`, + [user.id], + (updateErr) => { + if (updateErr) console.error('❌ Ошибка обновления last_chat_notification_sent_at:', updateErr); + else console.log(`✅ Сброшен счётчик уведомлений чата для пользователя ${user.login}`); + } + ); + } else { + // Создаём запись с настройками по умолчанию и проставляем last_chat_notification_sent_at + db.run( + `INSERT INTO user_settings + (user_id, email_notifications, notification_email, + telegram_notifications, telegram_chat_id, + vk_notifications, vk_user_id, last_chat_notification_sent_at) + VALUES (?, 1, ?, 0, '', 0, '', CURRENT_TIMESTAMP)`, + [user.id, user.email || ''], + (insertErr) => { + if (insertErr) console.error('❌ Ошибка создания user_settings:', insertErr); + else console.log(`✅ Созданы настройки и сброшен счётчик уведомлений чата для пользователя ${user.login}`); + } + ); + } + }); + } else { + console.warn('⚠️ База данных не доступна, пропускаем обновление last_chat_notification_sent_at'); + } + req.session.save((err) => { if (err) { console.error('❌ Ошибка сохранения сессии:', err); @@ -245,9 +285,9 @@ app.post('/api/login', async (req, res) => { if (user.groups) { console.log(`Группы пользователя: ${user.groups}`); } - - res.json({ - success: true, + + res.json({ + success: true, user: sessionUser }); }); @@ -257,9 +297,9 @@ app.post('/api/login', async (req, res) => { } } catch (error) { console.error('❌ Ошибка аутентификации:', error.message); - res.status(500).json({ + res.status(500).json({ error: 'Ошибка сервера при авторизации', - details: error.message + details: error.message }); } }); @@ -1595,6 +1635,9 @@ initializeServer().then(() => { setInterval(checkOverdueTasks, 60000); setInterval(checkUpcomingDeadlines, 60000); setInterval(() => cronJobs.checkDocumentsForCompletion(db), 60000); + + // ✅ Запускаем cron уведомлений чата (раз в час) + cronJobs.startChatNotificationsCron(); }); }).catch(error => { console.error('❌ Не удалось запустить сервер:', error);