Files
telegram-management-system/marketing-agent/services/scheduler/src/models/ScheduledCampaign.js
你的用户名 237c7802e5
Some checks failed
Deploy / deploy (push) Has been cancelled
Initial commit: Telegram Management System
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>
2025-11-04 15:37:50 +08:00

313 lines
8.8 KiB
JavaScript

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);