This commit is contained in:
romantarkin 2025-07-22 14:43:26 +05:00
commit a93f949020
12 changed files with 1267 additions and 0 deletions

1054
mail-service/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

12
mail-service/package.json Normal file
View File

@ -0,0 +1,12 @@
{
"scripts": {
"start": "node src/index.js"
},
"type": "module",
"dependencies": {
"dotenv": "^17.2.0",
"express": "^5.1.0",
"mysql2": "^3.14.2",
"sequelize": "^6.37.7"
}
}

27
mail-service/src/index.js Normal file
View File

@ -0,0 +1,27 @@
import dotenv from 'dotenv';
dotenv.config();
import express from 'express';
import { sequelize } from './models/index.js';
const app = express();
app.use(express.json());
app.get('/', (req, res) => {
res.send('Mail Service is running');
});
(async () => {
try {
await sequelize.authenticate();
await sequelize.sync({ alter: true });
console.log('Database connected and models synced');
} catch (err) {
console.error('Unable to connect to the database:', err);
process.exit(1);
}
})();
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Mail Service listening on port ${PORT}`);
});

View File

@ -0,0 +1,15 @@
import { DataTypes, Sequelize } from 'sequelize';
export default (sequelize) => {
const Campaign = sequelize.define('Campaign', {
id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true },
user_id: { type: DataTypes.INTEGER, allowNull: false },
template_version_id: { type: DataTypes.INTEGER, allowNull: false },
group_id: { type: DataTypes.INTEGER, allowNull: false },
subject_override: { type: DataTypes.STRING },
scheduled_at: { type: DataTypes.DATE },
status: { type: DataTypes.ENUM('draft', 'scheduled', 'sent', 'failed'), defaultValue: 'draft' },
created_at: { type: DataTypes.DATE, defaultValue: Sequelize.NOW },
}, { tableName: 'campaigns', timestamps: false });
return Campaign;
};

View File

@ -0,0 +1,15 @@
import { DataTypes, Sequelize } from 'sequelize';
export default (sequelize) => {
const DeliveryLog = sequelize.define('DeliveryLog', {
id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true },
campaign_id: { type: DataTypes.INTEGER, allowNull: false },
subscriber_id: { type: DataTypes.INTEGER, allowNull: false },
sent_at: { type: DataTypes.DATE },
status: { type: DataTypes.ENUM('sent', 'bounced', 'failed'), allowNull: false },
error_message: { type: DataTypes.STRING },
opened_at: { type: DataTypes.DATE },
clicked_at: { type: DataTypes.DATE },
}, { tableName: 'delivery_logs', timestamps: false });
return DeliveryLog;
};

View File

@ -0,0 +1,12 @@
import { DataTypes, Sequelize } from 'sequelize';
export default (sequelize) => {
const EmailTemplate = sequelize.define('EmailTemplate', {
id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true },
user_id: { type: DataTypes.INTEGER, allowNull: false },
name: { type: DataTypes.STRING, allowNull: false },
created_at: { type: DataTypes.DATE, defaultValue: Sequelize.NOW },
updated_at: { type: DataTypes.DATE, defaultValue: Sequelize.NOW },
}, { tableName: 'email_templates', timestamps: false });
return EmailTemplate;
};

View File

@ -0,0 +1,15 @@
import { DataTypes, Sequelize } from 'sequelize';
export default (sequelize) => {
const EmailTemplateVersion = sequelize.define('EmailTemplateVersion', {
id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true },
template_id: { type: DataTypes.INTEGER, allowNull: false },
version_number: { type: DataTypes.INTEGER, allowNull: false },
subject: { type: DataTypes.STRING, allowNull: false },
body_html: { type: DataTypes.TEXT },
body_text: { type: DataTypes.TEXT },
is_active: { type: DataTypes.BOOLEAN, defaultValue: true },
created_at: { type: DataTypes.DATE, defaultValue: Sequelize.NOW },
}, { tableName: 'email_template_versions', timestamps: false });
return EmailTemplateVersion;
};

View File

@ -0,0 +1,11 @@
import { DataTypes, Sequelize } from 'sequelize';
export default (sequelize) => {
const GroupSubscriber = sequelize.define('GroupSubscriber', {
id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true },
group_id: { type: DataTypes.INTEGER, allowNull: false },
subscriber_id: { type: DataTypes.INTEGER, allowNull: false },
added_at: { type: DataTypes.DATE, defaultValue: Sequelize.NOW },
}, { tableName: 'group_subscribers', timestamps: false });
return GroupSubscriber;
};

View File

@ -0,0 +1,62 @@
import { Sequelize } from 'sequelize';
import SubscriberModel from './subscriber.js';
import MailingGroupModel from './mailingGroup.js';
import GroupSubscriberModel from './groupSubscriber.js';
import EmailTemplateModel from './emailTemplate.js';
import EmailTemplateVersionModel from './emailTemplateVersion.js';
import CampaignModel from './campaign.js';
import DeliveryLogModel from './deliveryLog.js';
import SmtpServerModel from './smtpServer.js';
const sequelize = new Sequelize(
process.env.DB_NAME,
process.env.DB_USER,
process.env.DB_PASSWORD,
{
host: process.env.DB_HOST,
port: process.env.DB_PORT,
dialect: 'mysql',
logging: false,
}
);
const Subscriber = SubscriberModel(sequelize);
const MailingGroup = MailingGroupModel(sequelize);
const GroupSubscriber = GroupSubscriberModel(sequelize);
const EmailTemplate = EmailTemplateModel(sequelize);
const EmailTemplateVersion = EmailTemplateVersionModel(sequelize);
const Campaign = CampaignModel(sequelize);
const DeliveryLog = DeliveryLogModel(sequelize);
const SmtpServer = SmtpServerModel(sequelize);
// Связи
MailingGroup.belongsToMany(Subscriber, { through: GroupSubscriber, foreignKey: 'group_id', otherKey: 'subscriber_id' });
Subscriber.belongsToMany(MailingGroup, { through: GroupSubscriber, foreignKey: 'subscriber_id', otherKey: 'group_id' });
GroupSubscriber.belongsTo(MailingGroup, { foreignKey: 'group_id' });
GroupSubscriber.belongsTo(Subscriber, { foreignKey: 'subscriber_id' });
EmailTemplate.hasMany(EmailTemplateVersion, { foreignKey: 'template_id' });
EmailTemplateVersion.belongsTo(EmailTemplate, { foreignKey: 'template_id' });
Campaign.belongsTo(EmailTemplateVersion, { foreignKey: 'template_version_id' });
Campaign.belongsTo(MailingGroup, { foreignKey: 'group_id' });
DeliveryLog.belongsTo(Campaign, { foreignKey: 'campaign_id' });
DeliveryLog.belongsTo(Subscriber, { foreignKey: 'subscriber_id' });
SmtpServer.belongsTo(MailingGroup, { foreignKey: 'group_id' });
MailingGroup.hasMany(SmtpServer, { foreignKey: 'group_id' });
// (связи с user_id только по полю, без внешнего ключа на User)
export {
sequelize,
Subscriber,
MailingGroup,
GroupSubscriber,
EmailTemplate,
EmailTemplateVersion,
Campaign,
DeliveryLog,
SmtpServer,
};

View File

@ -0,0 +1,12 @@
import { DataTypes, Sequelize } from 'sequelize';
export default (sequelize) => {
const MailingGroup = sequelize.define('MailingGroup', {
id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true },
user_id: { type: DataTypes.INTEGER, allowNull: false },
name: { type: DataTypes.STRING, allowNull: false },
description: { type: DataTypes.STRING },
created_at: { type: DataTypes.DATE, defaultValue: Sequelize.NOW },
}, { tableName: 'mailing_groups', timestamps: false });
return MailingGroup;
};

View File

@ -0,0 +1,19 @@
import { DataTypes, Sequelize } from 'sequelize';
export default (sequelize) => {
const SmtpServer = sequelize.define('SmtpServer', {
id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true },
user_id: { type: DataTypes.INTEGER, allowNull: false },
group_id: { type: DataTypes.INTEGER, allowNull: true }, // связь с группой подписчиков
name: { type: DataTypes.STRING, allowNull: false },
host: { type: DataTypes.STRING, allowNull: false },
port: { type: DataTypes.INTEGER, allowNull: false },
secure: { type: DataTypes.BOOLEAN, defaultValue: false },
username: { type: DataTypes.STRING, allowNull: false },
password: { type: DataTypes.STRING, allowNull: false },
from_email: { type: DataTypes.STRING, allowNull: false },
created_at: { type: DataTypes.DATE, defaultValue: Sequelize.NOW },
updated_at: { type: DataTypes.DATE, defaultValue: Sequelize.NOW },
}, { tableName: 'smtp_servers', timestamps: false });
return SmtpServer;
};

View File

@ -0,0 +1,13 @@
import { DataTypes, Sequelize } from 'sequelize';
export default (sequelize) => {
const Subscriber = sequelize.define('Subscriber', {
id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true },
email: { type: DataTypes.STRING, allowNull: false, unique: true },
name: { type: DataTypes.STRING },
status: { type: DataTypes.ENUM('active', 'unsubscribed', 'bounced'), defaultValue: 'active' },
created_at: { type: DataTypes.DATE, defaultValue: Sequelize.NOW },
unsubscribed_at: { type: DataTypes.DATE },
}, { tableName: 'subscribers', timestamps: false });
return Subscriber;
};