Initial commit: Telegram Management System
Some checks failed
Deploy / deploy (push) Has been cancelled
Some checks failed
Deploy / deploy (push) Has been cancelled
Full-stack web application for Telegram management - Frontend: Vue 3 + Vben Admin - Backend: NestJS - Features: User management, group broadcast, statistics 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,313 @@
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
const scheduledCampaignSchema = new mongoose.Schema({
|
||||
// Multi-tenant support
|
||||
tenantId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Tenant',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
accountId: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
campaignId: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
campaignName: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['one-time', 'recurring', 'trigger-based'],
|
||||
required: true
|
||||
},
|
||||
schedule: {
|
||||
// For one-time campaigns
|
||||
startDateTime: Date,
|
||||
|
||||
// For recurring campaigns
|
||||
recurring: {
|
||||
pattern: {
|
||||
type: String,
|
||||
enum: ['daily', 'weekly', 'monthly', 'custom']
|
||||
},
|
||||
frequency: {
|
||||
interval: Number, // e.g., every 2 days
|
||||
unit: {
|
||||
type: String,
|
||||
enum: ['minutes', 'hours', 'days', 'weeks', 'months']
|
||||
}
|
||||
},
|
||||
daysOfWeek: [Number], // 0-6 (Sunday-Saturday)
|
||||
daysOfMonth: [Number], // 1-31
|
||||
timeOfDay: String, // HH:MM format
|
||||
timezone: {
|
||||
type: String,
|
||||
default: 'UTC'
|
||||
},
|
||||
endDate: Date,
|
||||
maxOccurrences: Number
|
||||
},
|
||||
|
||||
// For trigger-based campaigns
|
||||
triggers: [{
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['user_event', 'date_based', 'condition_met', 'external_api']
|
||||
},
|
||||
conditions: mongoose.Schema.Types.Mixed,
|
||||
delay: {
|
||||
value: Number,
|
||||
unit: {
|
||||
type: String,
|
||||
enum: ['minutes', 'hours', 'days']
|
||||
}
|
||||
}
|
||||
}]
|
||||
},
|
||||
|
||||
targetAudience: {
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['all', 'segment', 'group', 'individual', 'dynamic']
|
||||
},
|
||||
segmentId: String,
|
||||
groupIds: [String],
|
||||
userIds: [String],
|
||||
dynamicCriteria: mongoose.Schema.Types.Mixed
|
||||
},
|
||||
|
||||
messageConfig: {
|
||||
templateId: String,
|
||||
content: String,
|
||||
personalization: {
|
||||
enabled: { type: Boolean, default: true },
|
||||
fields: [String]
|
||||
},
|
||||
variations: [{
|
||||
name: String,
|
||||
templateId: String,
|
||||
weight: Number // For A/B testing
|
||||
}]
|
||||
},
|
||||
|
||||
deliverySettings: {
|
||||
priority: {
|
||||
type: String,
|
||||
enum: ['low', 'normal', 'high', 'critical'],
|
||||
default: 'normal'
|
||||
},
|
||||
rateLimiting: {
|
||||
enabled: { type: Boolean, default: true },
|
||||
messagesPerHour: Number,
|
||||
messagesPerUser: Number
|
||||
},
|
||||
retryPolicy: {
|
||||
maxRetries: { type: Number, default: 3 },
|
||||
retryDelay: { type: Number, default: 300 } // seconds
|
||||
},
|
||||
quietHours: {
|
||||
enabled: { type: Boolean, default: true },
|
||||
start: String, // HH:MM format
|
||||
end: String,
|
||||
timezone: String
|
||||
}
|
||||
},
|
||||
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['draft', 'scheduled', 'active', 'paused', 'completed', 'cancelled', 'failed'],
|
||||
default: 'draft',
|
||||
index: true
|
||||
},
|
||||
|
||||
execution: {
|
||||
nextRunAt: Date,
|
||||
lastRunAt: Date,
|
||||
runCount: { type: Number, default: 0 },
|
||||
history: [{
|
||||
runAt: Date,
|
||||
status: String,
|
||||
messagesSent: Number,
|
||||
errors: Number,
|
||||
duration: Number,
|
||||
details: mongoose.Schema.Types.Mixed
|
||||
}]
|
||||
},
|
||||
|
||||
statistics: {
|
||||
totalRuns: { type: Number, default: 0 },
|
||||
totalMessagesSent: { type: Number, default: 0 },
|
||||
totalDelivered: { type: Number, default: 0 },
|
||||
totalFailed: { type: Number, default: 0 },
|
||||
avgDeliveryRate: { type: Number, default: 0 },
|
||||
lastUpdated: Date
|
||||
},
|
||||
|
||||
metadata: {
|
||||
createdBy: String,
|
||||
updatedBy: String,
|
||||
tags: [String],
|
||||
notes: String,
|
||||
integrations: [{
|
||||
type: String,
|
||||
config: mongoose.Schema.Types.Mixed
|
||||
}]
|
||||
},
|
||||
|
||||
notifications: {
|
||||
onStart: {
|
||||
enabled: { type: Boolean, default: false },
|
||||
channels: [String], // email, slack, webhook
|
||||
recipients: [String]
|
||||
},
|
||||
onComplete: {
|
||||
enabled: { type: Boolean, default: true },
|
||||
channels: [String],
|
||||
recipients: [String]
|
||||
},
|
||||
onFailure: {
|
||||
enabled: { type: Boolean, default: true },
|
||||
channels: [String],
|
||||
recipients: [String]
|
||||
}
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
// Indexes
|
||||
scheduledCampaignSchema.index({ accountId: 1, status: 1 });
|
||||
scheduledCampaignSchema.index({ 'execution.nextRunAt': 1, status: 1 });
|
||||
scheduledCampaignSchema.index({ type: 1, status: 1 });
|
||||
|
||||
// Multi-tenant indexes
|
||||
scheduledCampaignSchema.index({ tenantId: 1, accountId: 1, status: 1 });
|
||||
scheduledCampaignSchema.index({ tenantId: 1, 'execution.nextRunAt': 1, status: 1 });
|
||||
scheduledCampaignSchema.index({ tenantId: 1, type: 1, status: 1 });
|
||||
|
||||
// Virtual for isActive
|
||||
scheduledCampaignSchema.virtual('isActive').get(function() {
|
||||
return this.status === 'active' || this.status === 'scheduled';
|
||||
});
|
||||
|
||||
// Methods
|
||||
scheduledCampaignSchema.methods.calculateNextRun = function() {
|
||||
if (this.type === 'one-time') {
|
||||
return this.schedule.startDateTime;
|
||||
}
|
||||
|
||||
if (this.type === 'recurring') {
|
||||
const now = new Date();
|
||||
const recurring = this.schedule.recurring;
|
||||
|
||||
// Implementation for calculating next run based on pattern
|
||||
// This would use date-fns or similar library for complex date calculations
|
||||
// Simplified version here
|
||||
if (recurring.pattern === 'daily') {
|
||||
const next = new Date(now);
|
||||
next.setDate(next.getDate() + (recurring.frequency?.interval || 1));
|
||||
return next;
|
||||
}
|
||||
|
||||
// Add more pattern calculations...
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
scheduledCampaignSchema.methods.shouldRun = function() {
|
||||
const now = new Date();
|
||||
|
||||
if (this.status !== 'active' && this.status !== 'scheduled') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.execution.nextRunAt && this.execution.nextRunAt > now) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.type === 'recurring') {
|
||||
const recurring = this.schedule.recurring;
|
||||
|
||||
// Check end date
|
||||
if (recurring.endDate && recurring.endDate < now) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check max occurrences
|
||||
if (recurring.maxOccurrences && this.execution.runCount >= recurring.maxOccurrences) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
scheduledCampaignSchema.methods.recordExecution = async function(result) {
|
||||
this.execution.lastRunAt = new Date();
|
||||
this.execution.runCount += 1;
|
||||
|
||||
// Add to history
|
||||
this.execution.history.push({
|
||||
runAt: new Date(),
|
||||
status: result.status,
|
||||
messagesSent: result.messagesSent || 0,
|
||||
errors: result.errors || 0,
|
||||
duration: result.duration,
|
||||
details: result.details
|
||||
});
|
||||
|
||||
// Keep only last 100 executions
|
||||
if (this.execution.history.length > 100) {
|
||||
this.execution.history = this.execution.history.slice(-100);
|
||||
}
|
||||
|
||||
// Update statistics
|
||||
this.statistics.totalRuns += 1;
|
||||
this.statistics.totalMessagesSent += result.messagesSent || 0;
|
||||
this.statistics.totalDelivered += result.delivered || 0;
|
||||
this.statistics.totalFailed += result.failed || 0;
|
||||
this.statistics.lastUpdated = new Date();
|
||||
|
||||
if (this.statistics.totalMessagesSent > 0) {
|
||||
this.statistics.avgDeliveryRate =
|
||||
(this.statistics.totalDelivered / this.statistics.totalMessagesSent) * 100;
|
||||
}
|
||||
|
||||
// Calculate next run
|
||||
if (this.type === 'recurring' && result.status === 'success') {
|
||||
this.execution.nextRunAt = this.calculateNextRun();
|
||||
} else if (this.type === 'one-time') {
|
||||
this.status = 'completed';
|
||||
}
|
||||
|
||||
await this.save();
|
||||
};
|
||||
|
||||
// Statics
|
||||
scheduledCampaignSchema.statics.findDueSchedules = async function(limit = 100) {
|
||||
const now = new Date();
|
||||
return this.find({
|
||||
status: { $in: ['active', 'scheduled'] },
|
||||
$or: [
|
||||
{ 'execution.nextRunAt': { $lte: now } },
|
||||
{ 'execution.nextRunAt': null }
|
||||
]
|
||||
})
|
||||
.limit(limit)
|
||||
.sort({ 'execution.nextRunAt': 1 });
|
||||
};
|
||||
|
||||
scheduledCampaignSchema.statics.findByAccountAndStatus = async function(accountId, status) {
|
||||
return this.find({ accountId, status })
|
||||
.sort({ createdAt: -1 });
|
||||
};
|
||||
|
||||
export default mongoose.model('ScheduledCampaign', scheduledCampaignSchema);
|
||||
Reference in New Issue
Block a user