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,330 @@
import { Campaign } from '../models/Campaign.js';
import { CampaignExecutor } from '../services/CampaignExecutor.js';
import { logger } from '../utils/logger.js';
import Boom from '@hapi/boom';
const campaignExecutor = CampaignExecutor.getInstance();
const routes = [
{
method: 'GET',
path: '/campaigns',
options: {
handler: async (request, h) => {
try {
const { status, type, page = 1, limit = 10 } = request.query;
const where = {};
if (status) where.status = status;
if (type) where.type = type;
const offset = (page - 1) * limit;
const { count, rows: campaigns } = await Campaign.findAndCountAll({
where,
limit: parseInt(limit),
offset: parseInt(offset),
order: [['createdAt', 'DESC']]
});
return {
success: true,
data: {
campaigns,
total: count,
page: parseInt(page),
totalPages: Math.ceil(count / limit)
}
};
} catch (error) {
logger.error('Failed to fetch campaigns:', error);
throw Boom.internal('Failed to fetch campaigns');
}
}
}
},
{
method: 'GET',
path: '/campaigns/{id}',
options: {
handler: async (request, h) => {
try {
const campaign = await Campaign.findByPk(request.params.id);
if (!campaign) {
throw Boom.notFound('Campaign not found');
}
return {
success: true,
data: campaign
};
} catch (error) {
logger.error('Failed to fetch campaign:', error);
if (error.isBoom) throw error;
throw Boom.internal('Failed to fetch campaign');
}
}
}
},
{
method: 'POST',
path: '/campaigns',
options: {
handler: async (request, h) => {
try {
const {
name,
description,
type,
goals,
targetAudience,
message,
startDate,
endDate,
budget
} = request.payload;
const campaign = await Campaign.create({
name,
description,
type,
goals,
targetAudience,
metadata: { message },
startDate,
endDate,
budget,
createdBy: request.auth.credentials?.user?.id || 'system',
status: 'draft'
});
return h.response({
success: true,
data: campaign
}).code(201);
} catch (error) {
logger.error('Failed to create campaign:', error);
throw Boom.internal('Failed to create campaign');
}
}
}
},
{
method: 'PUT',
path: '/campaigns/{id}',
options: {
handler: async (request, h) => {
try {
const campaign = await Campaign.findByPk(request.params.id);
if (!campaign) {
throw Boom.notFound('Campaign not found');
}
// Don't allow updating active campaigns
if (campaign.status === 'active') {
throw Boom.badRequest('Cannot update active campaign');
}
const allowedUpdates = [
'name', 'description', 'goals', 'targetAudience',
'startDate', 'endDate', 'budget', 'metadata'
];
allowedUpdates.forEach(field => {
if (request.payload[field] !== undefined) {
campaign[field] = request.payload[field];
}
});
await campaign.save();
return {
success: true,
data: campaign
};
} catch (error) {
logger.error('Failed to update campaign:', error);
if (error.isBoom) throw error;
throw Boom.internal('Failed to update campaign');
}
}
}
},
{
method: 'DELETE',
path: '/campaigns/{id}',
options: {
handler: async (request, h) => {
try {
const campaign = await Campaign.findByPk(request.params.id);
if (!campaign) {
throw Boom.notFound('Campaign not found');
}
// Don't allow deleting active campaigns
if (campaign.status === 'active') {
throw Boom.badRequest('Cannot delete active campaign');
}
await campaign.destroy();
return {
success: true,
message: 'Campaign deleted successfully'
};
} catch (error) {
logger.error('Failed to delete campaign:', error);
if (error.isBoom) throw error;
throw Boom.internal('Failed to delete campaign');
}
}
}
},
{
method: 'POST',
path: '/campaigns/{id}/execute',
options: {
handler: async (request, h) => {
try {
const result = await campaignExecutor.executeCampaign(request.params.id);
return {
success: true,
data: result
};
} catch (error) {
logger.error('Failed to execute campaign:', error);
throw Boom.internal(error.message || 'Failed to execute campaign');
}
}
}
},
{
method: 'POST',
path: '/campaigns/{id}/pause',
options: {
handler: async (request, h) => {
try {
const result = await campaignExecutor.pauseCampaign(request.params.id);
return {
success: true,
data: result
};
} catch (error) {
logger.error('Failed to pause campaign:', error);
throw Boom.internal(error.message || 'Failed to pause campaign');
}
}
}
},
{
method: 'POST',
path: '/campaigns/{id}/resume',
options: {
handler: async (request, h) => {
try {
const result = await campaignExecutor.resumeCampaign(request.params.id);
return {
success: true,
data: result
};
} catch (error) {
logger.error('Failed to resume campaign:', error);
throw Boom.internal(error.message || 'Failed to resume campaign');
}
}
}
},
{
method: 'GET',
path: '/campaigns/{id}/progress',
options: {
handler: async (request, h) => {
try {
const progress = await campaignExecutor.getCampaignProgress(request.params.id);
return {
success: true,
data: progress
};
} catch (error) {
logger.error('Failed to get campaign progress:', error);
throw Boom.internal(error.message || 'Failed to get campaign progress');
}
}
}
},
{
method: 'GET',
path: '/campaigns/{id}/statistics',
options: {
handler: async (request, h) => {
try {
const campaign = await Campaign.findByPk(request.params.id);
if (!campaign) {
throw Boom.notFound('Campaign not found');
}
return {
success: true,
data: campaign.statistics || {}
};
} catch (error) {
logger.error('Failed to get campaign statistics:', error);
if (error.isBoom) throw error;
throw Boom.internal('Failed to get campaign statistics');
}
}
}
},
{
method: 'POST',
path: '/campaigns/{id}/clone',
options: {
handler: async (request, h) => {
try {
const sourceCampaign = await Campaign.findByPk(request.params.id);
if (!sourceCampaign) {
throw Boom.notFound('Campaign not found');
}
const clonedData = sourceCampaign.toJSON();
delete clonedData.id;
delete clonedData.createdAt;
delete clonedData.updatedAt;
clonedData.name = `${clonedData.name} (Copy)`;
clonedData.status = 'draft';
clonedData.statistics = {
totalTasks: 0,
completedTasks: 0,
failedTasks: 0,
messagesSent: 0,
conversionsAchieved: 0,
totalCost: 0
};
const clonedCampaign = await Campaign.create(clonedData);
return h.response({
success: true,
data: clonedCampaign
}).code(201);
} catch (error) {
logger.error('Failed to clone campaign:', error);
if (error.isBoom) throw error;
throw Boom.internal('Failed to clone campaign');
}
}
}
}
];
export default routes;

View File

@@ -0,0 +1,11 @@
import campaignRoutes from './campaigns.js';
import messageRoutes from './messages.js';
import workflowRoutes from './workflows.js';
const routes = [
...campaignRoutes,
...messageRoutes,
...workflowRoutes
];
export default routes;

View File

@@ -0,0 +1,481 @@
import { logger } from '../utils/logger.js';
import axios from 'axios';
import Boom from '@hapi/boom';
import { TemplateClient } from '../../../../shared/utils/templateClient.js';
// Initialize template client
const templateClient = new TemplateClient({
baseURL: process.env.TEMPLATE_SERVICE_URL || 'http://localhost:3010'
});
// In-memory storage for templates (should be moved to database)
const messageTemplates = new Map();
// Predefined templates
const defaultTemplates = [
{
id: 'welcome-1',
name: 'Welcome Message',
content: 'Welcome to {{company}}! 👋\n\nWe\'re excited to have you here. {{personalized_message}}',
category: 'onboarding',
variables: ['company', 'personalized_message'],
usage: 0
},
{
id: 'promo-1',
name: 'Special Offer',
content: '🎉 Special Offer Alert!\n\n{{offer_title}}\n\n✨ {{discount}}% OFF\n📅 Valid until: {{end_date}}\n\n👉 {{cta_text}}',
category: 'promotional',
variables: ['offer_title', 'discount', 'end_date', 'cta_text'],
usage: 0
},
{
id: 'reminder-1',
name: 'Event Reminder',
content: '⏰ Reminder: {{event_name}}\n\n📅 Date: {{event_date}}\n📍 Location: {{location}}\n\nDon\'t forget to {{action}}!',
category: 'reminder',
variables: ['event_name', 'event_date', 'location', 'action'],
usage: 0
},
{
id: 'survey-1',
name: 'Feedback Request',
content: 'Hi {{name}}! 👋\n\nWe\'d love to hear your thoughts about {{product_service}}.\n\nCould you spare 2 minutes for a quick survey?\n\n🔗 {{survey_link}}\n\nThank you! 🙏',
category: 'engagement',
variables: ['name', 'product_service', 'survey_link'],
usage: 0
}
];
// Initialize default templates
defaultTemplates.forEach(template => {
messageTemplates.set(template.id, template);
});
const routes = [
{
method: 'GET',
path: '/messages/templates',
options: {
handler: async (request, h) => {
try {
const { category } = request.query;
let templates = Array.from(messageTemplates.values());
if (category) {
templates = templates.filter(t => t.category === category);
}
return {
success: true,
data: templates
};
} catch (error) {
logger.error('Failed to fetch templates:', error);
throw Boom.internal('Failed to fetch templates');
}
}
}
},
{
method: 'GET',
path: '/messages/templates/{id}',
options: {
handler: async (request, h) => {
try {
const template = messageTemplates.get(request.params.id);
if (!template) {
throw Boom.notFound('Template not found');
}
return {
success: true,
data: template
};
} catch (error) {
logger.error('Failed to fetch template:', error);
if (error.isBoom) throw error;
throw Boom.internal('Failed to fetch template');
}
}
}
},
{
method: 'POST',
path: '/messages/templates',
options: {
handler: async (request, h) => {
try {
const { name, content, category, variables = [] } = request.payload;
if (!name || !content || !category) {
throw Boom.badRequest('Missing required fields: name, content, category');
}
const template = {
id: `custom-${Date.now()}`,
name,
content,
category,
variables,
usage: 0,
createdAt: new Date().toISOString()
};
messageTemplates.set(template.id, template);
return h.response({
success: true,
data: template
}).code(201);
} catch (error) {
logger.error('Failed to create template:', error);
if (error.isBoom) throw error;
throw Boom.internal('Failed to create template');
}
}
}
},
{
method: 'PUT',
path: '/messages/templates/{id}',
options: {
handler: async (request, h) => {
try {
const template = messageTemplates.get(request.params.id);
if (!template) {
throw Boom.notFound('Template not found');
}
const { name, content, category, variables } = request.payload;
if (name) template.name = name;
if (content) template.content = content;
if (category) template.category = category;
if (variables) template.variables = variables;
template.updatedAt = new Date().toISOString();
return {
success: true,
data: template
};
} catch (error) {
logger.error('Failed to update template:', error);
if (error.isBoom) throw error;
throw Boom.internal('Failed to update template');
}
}
}
},
{
method: 'DELETE',
path: '/messages/templates/{id}',
options: {
handler: async (request, h) => {
try {
if (!messageTemplates.has(request.params.id)) {
throw Boom.notFound('Template not found');
}
messageTemplates.delete(request.params.id);
return {
success: true,
message: 'Template deleted successfully'
};
} catch (error) {
logger.error('Failed to delete template:', error);
if (error.isBoom) throw error;
throw Boom.internal('Failed to delete template');
}
}
}
},
{
method: 'POST',
path: '/messages/templates/{id}/preview',
options: {
handler: async (request, h) => {
try {
const template = messageTemplates.get(request.params.id);
if (!template) {
throw Boom.notFound('Template not found');
}
const { variables } = request.payload;
let content = template.content;
// Replace variables
if (variables) {
Object.entries(variables).forEach(([key, value]) => {
const regex = new RegExp(`{{\\s*${key}\\s*}}`, 'g');
content = content.replace(regex, value);
});
}
return {
success: true,
data: {
original: template.content,
preview: content,
variables: template.variables
}
};
} catch (error) {
logger.error('Failed to preview template:', error);
if (error.isBoom) throw error;
throw Boom.internal('Failed to preview template');
}
}
}
},
{
method: 'GET',
path: '/messages/history',
options: {
handler: async (request, h) => {
try {
// TODO: Implement actual message history from database
const mockHistory = [
{
id: 'msg-1',
campaignId: 'c1',
campaignName: 'Summer Sale 2025',
recipient: '+1234567890',
content: 'Special offer: 20% off all items!',
status: 'delivered',
sentAt: new Date(Date.now() - 3600000).toISOString(),
deliveredAt: new Date(Date.now() - 3590000).toISOString()
},
{
id: 'msg-2',
campaignId: 'c2',
campaignName: 'Welcome Series',
recipient: '+0987654321',
content: 'Welcome to our service!',
status: 'delivered',
sentAt: new Date(Date.now() - 7200000).toISOString(),
deliveredAt: new Date(Date.now() - 7190000).toISOString()
}
];
return {
success: true,
data: mockHistory
};
} catch (error) {
logger.error('Failed to fetch message history:', error);
throw Boom.internal('Failed to fetch message history');
}
}
}
},
{
method: 'POST',
path: '/messages/send',
options: {
handler: async (request, h) => {
try {
const { accountId, recipient, content, parseMode = 'md' } = request.payload;
if (!accountId || !recipient || !content) {
throw Boom.badRequest('Missing required fields: accountId, recipient, content');
}
// Send via gramjs-adapter
const gramjsUrl = process.env.GRAMJS_ADAPTER_URL || 'http://gramjs-adapter:3003';
const response = await axios.post(`${gramjsUrl}/messages/send`, {
accountId,
chatId: recipient,
message: content,
parseMode
});
return response.data;
} catch (error) {
logger.error('Failed to send message:', error);
if (error.response?.status === 400) {
throw Boom.badRequest(error.response.data.error || 'Invalid request');
}
throw Boom.internal(error.response?.data?.error || 'Failed to send message');
}
}
}
},
{
method: 'POST',
path: '/messages/send-with-template',
options: {
handler: async (request, h) => {
try {
const {
accountId,
recipient,
templateId,
templateName,
language = 'en',
data = {},
parseMode = 'md'
} = request.payload;
if (!accountId || !recipient || (!templateId && !templateName)) {
throw Boom.badRequest('Missing required fields: accountId, recipient, and either templateId or templateName');
}
// Render template
let renderResult;
if (templateId) {
renderResult = await templateClient.renderTemplate(templateId, data);
} else {
renderResult = await templateClient.renderTemplateByName(templateName, language, data);
}
if (!renderResult.success) {
throw Boom.badRequest(renderResult.error || 'Failed to render template');
}
// Send via gramjs-adapter
const gramjsUrl = process.env.GRAMJS_ADAPTER_URL || 'http://gramjs-adapter:3003';
const response = await axios.post(`${gramjsUrl}/messages/send`, {
accountId,
chatId: recipient,
message: renderResult.content,
parseMode: renderResult.format === 'html' ? 'html' : parseMode
});
return {
...response.data,
template: {
id: templateId,
name: templateName,
format: renderResult.format
}
};
} catch (error) {
logger.error('Failed to send message with template:', error);
if (error.isBoom) throw error;
if (error.response?.status === 400) {
throw Boom.badRequest(error.response.data.error || 'Invalid request');
}
throw Boom.internal(error.response?.data?.error || 'Failed to send message with template');
}
}
}
},
{
method: 'POST',
path: '/messages/bulk',
options: {
handler: async (request, h) => {
try {
const { accountId, recipients, content, parseMode = 'md' } = request.payload;
if (!accountId || !recipients || !content) {
throw Boom.badRequest('Missing required fields: accountId, recipients, content');
}
// Send via gramjs-adapter
const gramjsUrl = process.env.GRAMJS_ADAPTER_URL || 'http://gramjs-adapter:3003';
const response = await axios.post(`${gramjsUrl}/messages/bulk`, {
accountId,
recipients,
message: content,
parseMode
});
return response.data;
} catch (error) {
logger.error('Failed to send bulk messages:', error);
throw Boom.internal(error.response?.data?.error || 'Failed to send bulk messages');
}
}
}
},
{
method: 'POST',
path: '/messages/bulk-with-template',
options: {
handler: async (request, h) => {
try {
const {
accountId,
recipients,
templateId,
templateName,
language = 'en',
data = {},
personalizedData = {},
parseMode = 'md'
} = request.payload;
if (!accountId || !recipients || (!templateId && !templateName)) {
throw Boom.badRequest('Missing required fields: accountId, recipients, and either templateId or templateName');
}
// Prepare batch render requests
const renderRequests = recipients.map(recipient => {
const recipientData = {
...data,
...(personalizedData[recipient] || {})
};
return {
templateId,
templateName,
language,
data: recipientData,
recipient
};
});
// Batch render templates
const batchResult = await templateClient.batchRenderTemplates(renderRequests);
if (!batchResult.success) {
throw Boom.badRequest(batchResult.error || 'Failed to render templates');
}
// Prepare messages for bulk send
const messages = batchResult.results.map(result => ({
recipient: result.recipient,
content: result.content,
format: result.format
}));
// Send via gramjs-adapter
const gramjsUrl = process.env.GRAMJS_ADAPTER_URL || 'http://gramjs-adapter:3003';
const response = await axios.post(`${gramjsUrl}/messages/bulk-custom`, {
accountId,
messages: messages.map(msg => ({
chatId: msg.recipient,
message: msg.content,
parseMode: msg.format === 'html' ? 'html' : parseMode
}))
});
return {
...response.data,
template: {
id: templateId,
name: templateName,
recipientCount: recipients.length
}
};
} catch (error) {
logger.error('Failed to send bulk messages with template:', error);
if (error.isBoom) throw error;
if (error.response?.status === 400) {
throw Boom.badRequest(error.response.data.error || 'Invalid request');
}
throw Boom.internal(error.response?.data?.error || 'Failed to send bulk messages with template');
}
}
}
}
];
export default routes;

View File

@@ -0,0 +1,25 @@
import { logger } from '../utils/logger.js';
import Boom from '@hapi/boom';
const routes = [
{
method: 'GET',
path: '/workflows',
options: {
handler: async (request, h) => {
try {
// TODO: Implement workflow management
return {
success: true,
data: []
};
} catch (error) {
logger.error('Failed to fetch workflows:', error);
throw Boom.internal('Failed to fetch workflows');
}
}
}
}
];
export default routes;