unsub
This commit is contained in:
parent
8a425b16e7
commit
726ec8bd38
@ -2,39 +2,16 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
<link rel="icon" href="%PUBLIC_URL%/logo.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<meta name="description" content="Web site created using create-react-app" />
|
<meta name="description" content="Web site created using create-react-app" />
|
||||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo.png" />
|
||||||
<!--
|
|
||||||
manifest.json provides metadata used when your web app is installed on a
|
|
||||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
|
||||||
-->
|
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
<!--
|
|
||||||
Notice the use of %PUBLIC_URL% in the tags above.
|
|
||||||
It will be replaced with the URL of the `public` folder during the build.
|
|
||||||
Only files inside the `public` folder can be referenced from the HTML.
|
|
||||||
|
|
||||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
|
||||||
work correctly both with client-side routing and a non-root public URL.
|
|
||||||
Learn how to configure a non-root public URL by running `npm run build`.
|
|
||||||
-->
|
|
||||||
<title>React App</title>
|
<title>React App</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<!--
|
|
||||||
This HTML file is a template.
|
|
||||||
If you open it directly in the browser, you will see an empty page.
|
|
||||||
|
|
||||||
You can add webfonts, meta tags, or analytics to this file.
|
|
||||||
The build step will place the bundled scripts into the <body> tag.
|
|
||||||
|
|
||||||
To begin the development, run `npm start` or `yarn start`.
|
|
||||||
To create a production bundle, use `npm run build` or `yarn build`.
|
|
||||||
-->
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
BIN
frontend/public/logo.ico
Normal file
BIN
frontend/public/logo.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 69 KiB |
BIN
frontend/public/logo.png
Normal file
BIN
frontend/public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
@ -216,5 +216,74 @@ export default {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(500).json({ error: err.message });
|
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 });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -7,6 +7,7 @@ router.post('/', deliveryLogController.create);
|
|||||||
router.get('/', deliveryLogController.getAll);
|
router.get('/', deliveryLogController.getAll);
|
||||||
router.get('/pending-count', deliveryLogController.getPendingCount);
|
router.get('/pending-count', deliveryLogController.getPendingCount);
|
||||||
router.get('/statistics', deliveryLogController.getDeliveryStatistics);
|
router.get('/statistics', deliveryLogController.getDeliveryStatistics);
|
||||||
|
router.get('/subscriber-status', deliveryLogController.getSubscriberStatusStatistics);
|
||||||
router.get('/campaign/:campaignId', deliveryLogController.getLogsByCampaign);
|
router.get('/campaign/:campaignId', deliveryLogController.getLogsByCampaign);
|
||||||
router.get('/:id', deliveryLogController.getById);
|
router.get('/:id', deliveryLogController.getById);
|
||||||
router.put('/:id', deliveryLogController.update);
|
router.put('/:id', deliveryLogController.update);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Kafka } from 'kafkajs';
|
import { Kafka } from 'kafkajs';
|
||||||
import nodemailer from 'nodemailer';
|
import nodemailer from 'nodemailer';
|
||||||
import { SmtpServer, DeliveryLog } from '../models/index.js';
|
import { SmtpServer, DeliveryLog, Subscriber } from '../models/index.js';
|
||||||
import { topicManager } from './topicManager.js';
|
import { topicManager } from './topicManager.js';
|
||||||
|
|
||||||
const kafka = new Kafka({
|
const kafka = new Kafka({
|
||||||
@ -201,6 +201,9 @@ async function processEmailTask(task, topic) {
|
|||||||
status: 'failed',
|
status: 'failed',
|
||||||
error_message: errorMsg
|
error_message: errorMsg
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Устанавливаем статус подписчика как "unsubscribed"
|
||||||
|
await updateSubscriberStatus(task.subscriberId, 'unsubscribed', errorMsg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -237,6 +240,9 @@ async function processEmailTask(task, topic) {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error sending email:', err, 'task:', task);
|
console.error('Error sending email:', err, 'task:', task);
|
||||||
|
|
||||||
|
// Определяем тип ошибки и соответствующий статус
|
||||||
|
const { status, reason } = analyzeSmtpError(err);
|
||||||
|
|
||||||
// Обновляем запись в DeliveryLog с ошибкой
|
// Обновляем запись в DeliveryLog с ошибкой
|
||||||
if (deliveryLog) {
|
if (deliveryLog) {
|
||||||
await deliveryLog.update({
|
await deliveryLog.update({
|
||||||
@ -244,6 +250,87 @@ async function processEmailTask(task, topic) {
|
|||||||
error_message: err.message
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user