Initial commit: Telegram Management System
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:
你的用户名
2025-11-04 15:37:50 +08:00
commit 237c7802e5
3674 changed files with 525172 additions and 0 deletions

View File

@@ -0,0 +1,329 @@
import mongoose from 'mongoose';
const contactSchema = new mongoose.Schema({
// Multi-tenant support
tenantId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Tenant',
required: true,
index: true
},
telegramId: {
type: String,
required: true,
unique: true,
index: true
},
accountId: {
type: String,
required: true,
index: true
},
username: {
type: String,
index: true
},
firstName: String,
lastName: String,
phoneNumber: {
type: String,
index: true
},
email: String,
bio: String,
profilePhoto: String,
language: {
type: String,
default: 'en'
},
status: {
type: String,
enum: ['active', 'inactive', 'blocked', 'deleted'],
default: 'active'
},
source: {
type: String,
enum: ['import', 'chat', 'group', 'channel', 'manual', 'api'],
default: 'manual'
},
tags: [{
type: String,
trim: true
}],
groups: [{
groupId: String,
groupName: String,
joinedAt: Date
}],
customFields: {
type: Map,
of: mongoose.Schema.Types.Mixed
},
preferences: {
messageFrequency: {
type: String,
enum: ['high', 'medium', 'low', 'none'],
default: 'medium'
},
preferredTime: {
start: String, // HH:mm format
end: String // HH:mm format
},
topics: [String],
optOut: {
type: Boolean,
default: false
}
},
engagement: {
lastSeen: Date,
lastMessageSent: Date,
lastMessageReceived: Date,
totalMessagesSent: {
type: Number,
default: 0
},
totalMessagesReceived: {
type: Number,
default: 0
},
totalInteractions: {
type: Number,
default: 0
},
averageResponseTime: {
type: Number, // in seconds
default: 0
},
engagementScore: {
type: Number,
min: 0,
max: 100,
default: 50
}
},
demographics: {
age: Number,
gender: {
type: String,
enum: ['male', 'female', 'other', 'prefer_not_to_say']
},
location: {
country: String,
region: String,
city: String,
timezone: String
},
interests: [String]
},
history: [{
event: String,
timestamp: Date,
details: mongoose.Schema.Types.Mixed
}],
notes: String,
importedAt: Date,
lastModified: Date
}, {
timestamps: true
});
// Indexes
contactSchema.index({ accountId: 1, status: 1 });
contactSchema.index({ accountId: 1, tags: 1 });
contactSchema.index({ accountId: 1, 'groups.groupId': 1 });
contactSchema.index({ accountId: 1, 'engagement.engagementScore': -1 });
contactSchema.index({ accountId: 1, createdAt: -1 });
// Multi-tenant indexes
contactSchema.index({ tenantId: 1, accountId: 1, status: 1 });
contactSchema.index({ tenantId: 1, accountId: 1, tags: 1 });
contactSchema.index({ tenantId: 1, accountId: 1, 'groups.groupId': 1 });
contactSchema.index({ tenantId: 1, accountId: 1, 'engagement.engagementScore': -1 });
contactSchema.index({ tenantId: 1, accountId: 1, createdAt: -1 });
// Virtual properties
contactSchema.virtual('fullName').get(function() {
const parts = [];
if (this.firstName) parts.push(this.firstName);
if (this.lastName) parts.push(this.lastName);
return parts.join(' ') || this.username || this.telegramId;
});
contactSchema.virtual('isActive').get(function() {
return this.status === 'active' && !this.preferences.optOut;
});
contactSchema.virtual('daysSinceLastSeen').get(function() {
if (!this.engagement.lastSeen) return null;
const days = Math.floor((Date.now() - this.engagement.lastSeen) / (1000 * 60 * 60 * 24));
return days;
});
// Methods
contactSchema.methods.addTag = function(tag) {
if (!this.tags.includes(tag)) {
this.tags.push(tag);
}
};
contactSchema.methods.removeTag = function(tag) {
this.tags = this.tags.filter(t => t !== tag);
};
contactSchema.methods.updateEngagement = function(eventType) {
this.engagement.lastSeen = new Date();
this.engagement.totalInteractions += 1;
if (eventType === 'message_sent') {
this.engagement.lastMessageSent = new Date();
this.engagement.totalMessagesSent += 1;
} else if (eventType === 'message_received') {
this.engagement.lastMessageReceived = new Date();
this.engagement.totalMessagesReceived += 1;
}
// Update engagement score based on recent activity
this.calculateEngagementScore();
};
contactSchema.methods.calculateEngagementScore = function() {
let score = 50; // Base score
// Recent activity bonus
const daysSinceLastSeen = this.daysSinceLastSeen;
if (daysSinceLastSeen !== null) {
if (daysSinceLastSeen < 7) score += 20;
else if (daysSinceLastSeen < 30) score += 10;
else if (daysSinceLastSeen > 90) score -= 20;
}
// Message interaction bonus
if (this.engagement.totalMessagesReceived > 0) {
const responseRate = this.engagement.totalMessagesReceived / this.engagement.totalMessagesSent;
score += Math.min(20, responseRate * 20);
}
// Frequency bonus
const interactionsPerDay = this.engagement.totalInteractions /
Math.max(1, Math.floor((Date.now() - this.createdAt) / (1000 * 60 * 60 * 24)));
if (interactionsPerDay > 1) score += 10;
else if (interactionsPerDay < 0.1) score -= 10;
// Ensure score is within bounds
this.engagement.engagementScore = Math.max(0, Math.min(100, Math.round(score)));
};
contactSchema.methods.logHistory = function(event, details = {}) {
this.history.push({
event,
timestamp: new Date(),
details
});
// Keep only last 100 history entries
if (this.history.length > 100) {
this.history = this.history.slice(-100);
}
};
// Static methods
contactSchema.statics.findByTags = function(accountId, tags) {
return this.find({
accountId,
tags: { $in: tags },
status: 'active'
});
};
contactSchema.statics.findByGroup = function(accountId, groupId) {
return this.find({
accountId,
'groups.groupId': groupId,
status: 'active'
});
};
contactSchema.statics.findEngaged = function(accountId, minScore = 70) {
return this.find({
accountId,
'engagement.engagementScore': { $gte: minScore },
status: 'active',
'preferences.optOut': false
});
};
contactSchema.statics.bulkImport = async function(accountId, contacts) {
const operations = contacts.map(contact => ({
updateOne: {
filter: { telegramId: contact.telegramId, accountId },
update: {
$set: {
...contact,
accountId,
importedAt: new Date(),
source: 'import'
}
},
upsert: true
}
}));
return this.bulkWrite(operations);
};
contactSchema.statics.getSegmentStats = async function(accountId) {
return this.aggregate([
{ $match: { accountId, status: 'active' } },
{
$facet: {
byEngagement: [
{
$bucket: {
groupBy: '$engagement.engagementScore',
boundaries: [0, 25, 50, 75, 100],
default: 'Unknown',
output: { count: { $sum: 1 } }
}
}
],
bySource: [
{
$group: {
_id: '$source',
count: { $sum: 1 }
}
}
],
byStatus: [
{
$group: {
_id: '$status',
count: { $sum: 1 }
}
}
],
topTags: [
{ $unwind: '$tags' },
{
$group: {
_id: '$tags',
count: { $sum: 1 }
}
},
{ $sort: { count: -1 } },
{ $limit: 10 }
]
}
}
]);
};
// Pre-save hook
contactSchema.pre('save', function(next) {
this.lastModified = new Date();
next();
});
export const Contact = mongoose.model('Contact', contactSchema);