diff --git a/frontend/public/index.html b/frontend/public/index.html index 6a9f8c2..3b3cc32 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -2,39 +2,16 @@ - + - - + - React App
- diff --git a/frontend/public/logo.ico b/frontend/public/logo.ico new file mode 100644 index 0000000..c4ec188 Binary files /dev/null and b/frontend/public/logo.ico differ diff --git a/frontend/public/logo.png b/frontend/public/logo.png new file mode 100644 index 0000000..32c5f00 Binary files /dev/null and b/frontend/public/logo.png differ diff --git a/mail-service/src/controllers/deliveryLogController.js b/mail-service/src/controllers/deliveryLogController.js index a2414c2..004d422 100644 --- a/mail-service/src/controllers/deliveryLogController.js +++ b/mail-service/src/controllers/deliveryLogController.js @@ -216,5 +216,74 @@ export default { } catch (err) { res.status(500).json({ error: err.message }); } + }, + + // Получить статистику по статусам подписчиков после ошибок + async getSubscriberStatusStatistics(req, res) { + try { + const { campaignId } = req.query; + + let whereClause = {}; + if (campaignId) { + whereClause.campaign_id = campaignId; + } + + // Получаем статистику по статусам подписчиков + const activeCount = await Subscriber.count({ + where: { status: 'active' } + }); + const unsubscribedCount = await Subscriber.count({ + where: { status: 'unsubscribed' } + }); + const bouncedCount = await Subscriber.count({ + where: { status: 'bounced' } + }); + + // Получаем статистику по причинам отписки (из DeliveryLog) + const failedLogs = await DeliveryLog.findAll({ + where: { + ...whereClause, + status: 'failed' + }, + include: [Subscriber], + attributes: [ + 'error_message', + [sequelize.fn('COUNT', sequelize.col('id')), 'count'] + ], + group: ['error_message'], + order: [[sequelize.fn('COUNT', sequelize.col('id')), 'DESC']], + limit: 10, + raw: true + }); + + // Получаем последние отписки + const recentUnsubscribes = await Subscriber.findAll({ + where: { + status: 'unsubscribed', + unsubscribed_at: { + [Op.not]: null + } + }, + order: [['unsubscribed_at', 'DESC']], + limit: 20 + }); + + res.json({ + subscriberStatus: { + active: activeCount, + unsubscribed: unsubscribedCount, + bounced: bouncedCount, + total: activeCount + unsubscribedCount + bouncedCount + }, + failedReasons: failedLogs, + recentUnsubscribes: recentUnsubscribes.map(sub => ({ + id: sub.id, + email: sub.email, + unsubscribed_at: sub.unsubscribed_at + })) + }); + } catch (err) { + res.status(500).json({ error: err.message }); + } } }; \ No newline at end of file diff --git a/mail-service/src/routes/deliveryLog.js b/mail-service/src/routes/deliveryLog.js index 5f1653b..fd5c9d6 100644 --- a/mail-service/src/routes/deliveryLog.js +++ b/mail-service/src/routes/deliveryLog.js @@ -7,6 +7,7 @@ router.post('/', deliveryLogController.create); router.get('/', deliveryLogController.getAll); router.get('/pending-count', deliveryLogController.getPendingCount); router.get('/statistics', deliveryLogController.getDeliveryStatistics); +router.get('/subscriber-status', deliveryLogController.getSubscriberStatusStatistics); router.get('/campaign/:campaignId', deliveryLogController.getLogsByCampaign); router.get('/:id', deliveryLogController.getById); router.put('/:id', deliveryLogController.update); diff --git a/mail-service/src/service/dynamicConsumer.js b/mail-service/src/service/dynamicConsumer.js index 3738d06..c6f99e6 100644 --- a/mail-service/src/service/dynamicConsumer.js +++ b/mail-service/src/service/dynamicConsumer.js @@ -1,6 +1,6 @@ import { Kafka } from 'kafkajs'; import nodemailer from 'nodemailer'; -import { SmtpServer, DeliveryLog } from '../models/index.js'; +import { SmtpServer, DeliveryLog, Subscriber } from '../models/index.js'; import { topicManager } from './topicManager.js'; const kafka = new Kafka({ @@ -201,6 +201,9 @@ async function processEmailTask(task, topic) { status: 'failed', error_message: errorMsg }); + + // Устанавливаем статус подписчика как "unsubscribed" + await updateSubscriberStatus(task.subscriberId, 'unsubscribed', errorMsg); return; } @@ -237,6 +240,9 @@ async function processEmailTask(task, topic) { } catch (err) { console.error('Error sending email:', err, 'task:', task); + // Определяем тип ошибки и соответствующий статус + const { status, reason } = analyzeSmtpError(err); + // Обновляем запись в DeliveryLog с ошибкой if (deliveryLog) { await deliveryLog.update({ @@ -244,6 +250,87 @@ async function processEmailTask(task, topic) { error_message: err.message }); } + + // Обновляем статус подписчика в зависимости от типа ошибки + await updateSubscriberStatus(task.subscriberId, status, reason); + } +} + +// Функция для анализа SMTP ошибок и определения статуса подписчика +function analyzeSmtpError(error) { + const errorMessage = error.message.toLowerCase(); + const errorCode = error.code || ''; + + // Ошибки, указывающие на недействительный email или отписку + const unsubscribeErrors = [ + '550', '553', '554', // SMTP коды для недействительных адресов + 'user not found', + 'mailbox not found', + 'address not found', + 'recipient not found', + 'user unknown', + 'mailbox unavailable', + 'address rejected', + 'recipient rejected', + 'bounce', + 'hard bounce', + 'permanent failure' + ]; + + // Ошибки, указывающие на временные проблемы + const temporaryErrors = [ + '421', '450', '451', '452', // SMTP коды для временных ошибок + 'temporary failure', + 'temporarily unavailable', + 'try again later', + 'quota exceeded', + 'rate limit', + 'throttled' + ]; + + // Проверяем на ошибки отписки + for (const unsubscribeError of unsubscribeErrors) { + if (errorMessage.includes(unsubscribeError) || errorCode.includes(unsubscribeError)) { + return { + status: 'unsubscribed', + reason: `SMTP error: ${error.message}` + }; + } + } + + // Проверяем на временные ошибки + for (const tempError of temporaryErrors) { + if (errorMessage.includes(tempError) || errorCode.includes(tempError)) { + return { + status: 'bounced', + reason: `Temporary SMTP error: ${error.message}` + }; + } + } + + // По умолчанию устанавливаем статус "unsubscribed" для любых других ошибок + return { + status: 'unsubscribed', + reason: `SMTP error: ${error.message}` + }; +} + +// Функция для обновления статуса подписчика +async function updateSubscriberStatus(subscriberId, status, reason = null) { + try { + const subscriber = await Subscriber.findByPk(subscriberId); + if (subscriber) { + await subscriber.update({ + status: status, + unsubscribed_at: status === 'unsubscribed' ? new Date() : null + }); + + console.log(`[DynamicConsumer] Updated subscriber ${subscriberId} status to "${status}"${reason ? ` (reason: ${reason})` : ''}`); + } else { + console.error(`[DynamicConsumer] Subscriber ${subscriberId} not found`); + } + } catch (error) { + console.error(`[DynamicConsumer] Error updating subscriber ${subscriberId} status:`, error); } }