diff --git a/SMM_SYSTEM_README.md b/SMM_SYSTEM_README.md new file mode 100644 index 0000000..a89274b --- /dev/null +++ b/SMM_SYSTEM_README.md @@ -0,0 +1,246 @@ +# SMM Система Konturai + +Полная реализация системы управления социальными сетями для фронтенда Vue.js приложения. + +## 🚀 Реализованные компоненты + +### 1. Сервисы (Services) + +#### CampaignService + +- **Путь**: `src/service/CampaignService.js` +- **Функции**: CRUD операции для кампаний +- **API эндпоинты**: + - `GET /api/smm/campaigns` - получить все кампании + - `GET /api/smm/campaigns/{id}` - получить кампанию по ID + - `POST /api/smm/campaigns` - создать кампанию + - `PUT /api/smm/campaigns/{id}` - обновить кампанию + - `DELETE /api/smm/campaigns/{id}` - удалить кампанию + +#### ChannelService + +- **Путь**: `src/service/ChannelService.js` +- **Функции**: CRUD операции для каналов +- **API эндпоинты**: + - `GET /api/smm/channels` - получить все каналы + - `GET /api/smm/channels/{id}` - получить канал по ID + - `POST /api/smm/channels` - создать канал + - `PUT /api/smm/channels/{id}` - обновить канал + - `DELETE /api/smm/channels/{id}` - удалить канал + +#### ContentService + +- **Путь**: `src/service/ContentService.js` +- **Функции**: CRUD операции для контента, одобрение, получение сообщений +- **API эндпоинты**: + - `GET /api/smm/content` - получить весь контент + - `GET /api/smm/content/{id}` - получить контент по ID + - `POST /api/smm/content` - создать контент + - `PUT /api/smm/content/{id}` - обновить контент + - `DELETE /api/smm/content/{id}` - удалить контент + - `POST /api/smm/content/{id}/approve` - одобрить контент + - `GET /api/smm/content/{id}/messages` - получить сообщения контента + +#### PublishingService + +- **Путь**: `src/service/PublishingService.js` +- **Функции**: Публикация контента, проверка статуса, массовая публикация +- **API эндпоинты**: + - `POST /api/smm/publishing/post/{contentId}` - опубликовать контент + +#### AnalyticsService + +- **Путь**: `src/service/AnalyticsService.js` +- **Функции**: Сбор аналитики, получение статистики +- **API эндпоинты**: + - `POST /api/smm/analytics/collect-now` - запустить сбор аналитики + +### 2. Типы данных + +#### SMM Types + +- **Путь**: `src/types/smm.js` +- **Содержит**: + - `CampaignStatus` - статусы кампаний (PLANNED, ACTIVE, COMPLETED) + - `ChannelType` - типы каналов (TELEGRAM, VK, INSTAGRAM) + - `ContentStatus` - статусы контента (DRAFT, PENDING_APPROVAL, APPROVED, PUBLISHED, FAILED) + - DTO классы для всех сущностей + +### 3. Vue компоненты + +#### CampaignList + +- **Путь**: `src/views/smm/CampaignList.vue` +- **Функции**: Управление кампаниями с полным CRUD +- **Особенности**: + - Таблица с фильтрацией и сортировкой + - Диалоги создания/редактирования + - Валидация форм + - Подтверждение удаления + +#### ChannelList + +- **Путь**: `src/views/smm/ChannelList.vue` +- **Функции**: Управление каналами +- **Особенности**: + - Поддержка разных типов каналов + - Управление активностью каналов + - Валидация API ключей + +#### ContentList + +- **Путь**: `src/views/smm/ContentList.vue` +- **Функции**: Управление контентом +- **Особенности**: + - Создание и редактирование контента + - Одобрение контента + - Публикация контента + - Просмотр детальной информации + +#### AnalyticsDashboard + +- **Путь**: `src/views/smm/AnalyticsDashboard.vue` +- **Функции**: Аналитика и статистика +- **Особенности**: + - Статистические карточки + - Детальная статистика по всем модулям + - Ручной запуск сбора аналитики + +#### SmmDashboard + +- **Путь**: `src/views/smm/SmmDashboard.vue` +- **Функции**: Главная панель SMM системы +- **Особенности**: + - Обзор статистики + - Быстрые действия + - Последние кампании и контент + +### 4. Маршрутизация + +#### Добавленные маршруты + +```javascript +{ + path: '/smm', + name: 'smm-dashboard', + component: () => import('@/views/smm/SmmDashboard.vue'), + meta: { requiresAuth: true } +}, +{ + path: '/smm/campaigns', + name: 'smm-campaigns', + component: () => import('@/views/smm/CampaignList.vue'), + meta: { requiresAuth: true } +}, +{ + path: '/smm/channels', + name: 'smm-channels', + component: () => import('@/views/smm/ChannelList.vue'), + meta: { requiresAuth: true } +}, +{ + path: '/smm/content', + name: 'smm-content', + component: () => import('@/views/smm/ContentList.vue'), + meta: { requiresAuth: true } +}, +{ + path: '/smm/analytics', + name: 'smm-analytics', + component: () => import('@/views/smm/AnalyticsDashboard.vue'), + meta: { requiresAuth: true } +} +``` + +### 5. Навигация + +#### Обновленное меню + +Добавлен новый раздел "SMM Система" с пунктами: + +- SMM Панель (`/smm`) +- Кампании (`/smm/campaigns`) +- Каналы (`/smm/channels`) +- Контент (`/smm/content`) +- Аналитика (`/smm/analytics`) + +## 🔧 Технические особенности + +### Аутентификация + +- Все сервисы используют JWT токены из localStorage +- Автоматическая проверка авторизации +- Обработка ошибок аутентификации + +### Валидация + +- Клиентская валидация всех форм +- Проверка обязательных полей +- Валидация форматов данных + +### Обработка ошибок + +- Централизованная обработка ошибок +- Toast уведомления для пользователя +- Логирование ошибок в консоль + +### UI/UX + +- Современный дизайн с PrimeVue компонентами +- Адаптивная верстка +- Интуитивная навигация +- Подтверждения действий + +## 📋 Использование + +### 1. Управление кампаниями + +1. Перейдите в "SMM Система" → "Кампании" +2. Нажмите "Создать кампанию" для добавления новой кампании +3. Заполните форму с обязательными полями +4. Используйте фильтры и поиск для навигации по кампаниям + +### 2. Управление каналами + +1. Перейдите в "SMM Система" → "Каналы" +2. Добавьте каналы для Telegram, VK или Instagram +3. Укажите API ключи для каждого канала +4. Управляйте активностью каналов + +### 3. Управление контентом + +1. Перейдите в "SMM Система" → "Контент" +2. Создайте контент, привязав к кампании +3. Настройте время публикации и приоритет +4. Одобрите контент для публикации +5. Опубликуйте контент в каналы + +### 4. Аналитика + +1. Перейдите в "SMM Система" → "Аналитика" +2. Просматривайте статистику по всем модулям +3. Запускайте сбор аналитики вручную +4. Отслеживайте эффективность публикаций + +## 🚀 Готовность к использованию + +Система полностью готова к использованию и включает: + +- ✅ Все API эндпоинты из документации +- ✅ Полный CRUD для всех сущностей +- ✅ Современный UI с PrimeVue +- ✅ Адаптивный дизайн +- ✅ Обработка ошибок +- ✅ Валидация данных +- ✅ Навигация и маршрутизация + +## 🔗 Интеграция с бэкендом + +Система готова к интеграции с бэкендом по адресу: + +- **Базовый URL**: `https://api.konturai.kz/api/smm` +- **Аутентификация**: Bearer JWT токены +- **Формат данных**: JSON +- **Кодировка**: UTF-8 + +Все сервисы автоматически добавляют заголовки аутентификации и обрабатывают ответы API. diff --git a/src/config/api.js b/src/config/api.js index e88830f..2305e61 100644 --- a/src/config/api.js +++ b/src/config/api.js @@ -1,6 +1,7 @@ // API Configuration export const API_CONFIG = { BASE_URL: 'https://api.konturai.kz', + SMM_BASE_URL: 'https://api.konturai.kz/api/smm', ENDPOINTS: { // MarketItemController - Получение данных NEWS_ITEMS: '/api/parser/items', @@ -26,7 +27,28 @@ export const API_CONFIG = { // ReportController - Генерация и управление отчётами REPORT_GENERATE: '/api/parser/report/generate', REPORT_HISTORY: '/api/parser/report/history', - REPORT_DOWNLOAD: '/api/parser/report/history' + REPORT_DOWNLOAD: '/api/parser/report/history', + + // SMM System Endpoints + // CampaignController - Управление кампаниями + CAMPAIGNS: '/campaigns', + CAMPAIGN_BY_ID: '/campaigns', + + // ChannelController - Управление каналами + CHANNELS: '/channels', + CHANNEL_BY_ID: '/channels', + + // ContentController - Управление контентом + CONTENT: '/content', + CONTENT_BY_ID: '/content', + CONTENT_APPROVE: '/content', + CONTENT_MESSAGES: '/content', + + // PublishingController - Публикация контента + PUBLISHING_POST: '/publishing/post', + + // AnalyticsController - Аналитика + ANALYTICS_COLLECT: '/analytics/collect-now' } }; diff --git a/src/layout/AppMenu.vue b/src/layout/AppMenu.vue index 05d11e4..3f47190 100644 --- a/src/layout/AppMenu.vue +++ b/src/layout/AppMenu.vue @@ -26,6 +26,16 @@ const model = ref([ { label: 'История отчётов', icon: 'pi pi-history', to: '/reports/history' } ] }, + { + label: 'SMM Система', + items: [ + { label: 'SMM Панель', icon: 'pi pi-fw pi-home', to: '/smm' }, + { label: 'Кампании', icon: 'pi pi-fw pi-megaphone', to: '/smm/campaigns' }, + { label: 'Каналы', icon: 'pi pi-fw pi-broadcast-tower', to: '/smm/channels' }, + { label: 'Контент', icon: 'pi pi-fw pi-file-edit', to: '/smm/content' }, + { label: 'Аналитика', icon: 'pi pi-fw pi-chart-line', to: '/smm/analytics' } + ] + }, { label: 'Admin', items: [ diff --git a/src/router/index.js b/src/router/index.js index d0e623b..2f7d25e 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -167,6 +167,36 @@ const router = createRouter({ name: 'reports-history', component: () => import('@/views/reports/ReportHistory.vue'), meta: { requiresAuth: true } + }, + { + path: '/smm/campaigns', + name: 'smm-campaigns', + component: () => import('@/views/smm/CampaignList.vue'), + meta: { requiresAuth: true } + }, + { + path: '/smm/channels', + name: 'smm-channels', + component: () => import('@/views/smm/ChannelList.vue'), + meta: { requiresAuth: true } + }, + { + path: '/smm/content', + name: 'smm-content', + component: () => import('@/views/smm/ContentList.vue'), + meta: { requiresAuth: true } + }, + { + path: '/smm/analytics', + name: 'smm-analytics', + component: () => import('@/views/smm/AnalyticsDashboard.vue'), + meta: { requiresAuth: true } + }, + { + path: '/smm', + name: 'smm-dashboard', + component: () => import('@/views/smm/SmmDashboard.vue'), + meta: { requiresAuth: true } } ] }, diff --git a/src/service/AnalyticsService.js b/src/service/AnalyticsService.js new file mode 100644 index 0000000..294a9f7 --- /dev/null +++ b/src/service/AnalyticsService.js @@ -0,0 +1,225 @@ +import { API_CONFIG, DEFAULT_REQUEST_CONFIG } from '@/config/api.js'; +import AuthService from '@/service/AuthService.js'; +import { AnalyticsResultDto } from '@/types/smm.js'; + +/** + * AnalyticsService - Аналитика + * Базовый путь: /api/smm/analytics + */ +class AnalyticsService { + constructor() { + this.baseUrl = API_CONFIG.SMM_BASE_URL; + this.endpoints = { + collectNow: API_CONFIG.ENDPOINTS.ANALYTICS_COLLECT + }; + } + + /** + * Запустить сбор аналитики + * POST /api/smm/analytics/collect-now + * @returns {Promise} + */ + async collectAnalytics() { + try { + const response = await AuthService.authFetch(`${this.baseUrl}${this.endpoints.collectNow}`, { + method: 'POST', + headers: { + ...DEFAULT_REQUEST_CONFIG.headers + } + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(`HTTP error! status: ${response.status}, message: ${errorData.message || 'Unknown error'}`); + } + + const data = await response.json(); + return new AnalyticsResultDto(data); + } catch (error) { + console.error('Error collecting analytics:', error); + throw error; + } + } + + /** + * Получить статистику кампаний + * @returns {Promise} - Статистика кампаний + */ + async getCampaignStats() { + try { + // Импортируем CampaignService для получения данных + const { default: CampaignService } = await import('./CampaignService.js'); + const campaigns = await CampaignService.getAllCampaigns(); + + const stats = { + total: campaigns.length, + byStatus: {}, + totalBudget: 0, + activeCampaigns: 0, + completedCampaigns: 0, + plannedCampaigns: 0 + }; + + campaigns.forEach((campaign) => { + // Подсчет по статусам + stats.byStatus[campaign.status] = (stats.byStatus[campaign.status] || 0) + 1; + + // Подсчет бюджета + if (campaign.budget) { + stats.totalBudget += campaign.budget; + } + + // Подсчет активных кампаний + if (campaign.status === 'ACTIVE') { + stats.activeCampaigns++; + } else if (campaign.status === 'COMPLETED') { + stats.completedCampaigns++; + } else if (campaign.status === 'PLANNED') { + stats.plannedCampaigns++; + } + }); + + return stats; + } catch (error) { + console.error('Error getting campaign stats:', error); + throw error; + } + } + + /** + * Получить статистику каналов + * @returns {Promise} - Статистика каналов + */ + async getChannelStats() { + try { + // Импортируем ChannelService для получения данных + const { default: ChannelService } = await import('./ChannelService.js'); + const channels = await ChannelService.getAllChannels(); + + const stats = { + total: channels.length, + byType: {}, + activeChannels: 0, + inactiveChannels: 0 + }; + + channels.forEach((channel) => { + // Подсчет по типам + stats.byType[channel.type] = (stats.byType[channel.type] || 0) + 1; + + // Подсчет активных/неактивных + if (channel.isActive) { + stats.activeChannels++; + } else { + stats.inactiveChannels++; + } + }); + + return stats; + } catch (error) { + console.error('Error getting channel stats:', error); + throw error; + } + } + + /** + * Получить статистику контента + * @returns {Promise} - Статистика контента + */ + async getContentStats() { + try { + // Импортируем ContentService для получения данных + const { default: ContentService } = await import('./ContentService.js'); + const content = await ContentService.getAllContent(); + + const stats = { + total: content.length, + byStatus: {}, + byLocale: {}, + pendingApproval: 0, + approved: 0, + published: 0, + failed: 0 + }; + + content.forEach((item) => { + // Подсчет по статусам + stats.byStatus[item.status] = (stats.byStatus[item.status] || 0) + 1; + + // Подсчет по локалям + stats.byLocale[item.locale] = (stats.byLocale[item.locale] || 0) + 1; + + // Подсчет специальных статусов + if (item.status === 'PENDING_APPROVAL') { + stats.pendingApproval++; + } else if (item.status === 'APPROVED') { + stats.approved++; + } else if (item.status === 'PUBLISHED') { + stats.published++; + } else if (item.status === 'FAILED') { + stats.failed++; + } + }); + + return stats; + } catch (error) { + console.error('Error getting content stats:', error); + throw error; + } + } + + /** + * Получить общую статистику системы + * @returns {Promise} - Общая статистика + */ + async getSystemStats() { + try { + const [campaignStats, channelStats, contentStats] = await Promise.all([this.getCampaignStats(), this.getChannelStats(), this.getContentStats()]); + + return { + campaigns: campaignStats, + channels: channelStats, + content: contentStats, + timestamp: new Date().toISOString() + }; + } catch (error) { + console.error('Error getting system stats:', error); + throw error; + } + } + + /** + * Получить статистику публикаций + * @returns {Promise} - Статистика публикаций + */ + async getPublishingStats() { + try { + // Импортируем ContentService для получения данных + const { default: ContentService } = await import('./ContentService.js'); + const content = await ContentService.getAllContent(); + + const publishedContent = content.filter((item) => item.status === 'PUBLISHED'); + const stats = { + totalPublished: publishedContent.length, + totalContent: content.length, + publishingRate: content.length > 0 ? (publishedContent.length / content.length) * 100 : 0, + byCampaign: {}, + byLocale: {} + }; + + // Группировка по кампаниям + publishedContent.forEach((item) => { + stats.byCampaign[item.campaignId] = (stats.byCampaign[item.campaignId] || 0) + 1; + stats.byLocale[item.locale] = (stats.byLocale[item.locale] || 0) + 1; + }); + + return stats; + } catch (error) { + console.error('Error getting publishing stats:', error); + throw error; + } + } +} + +// Экспорт singleton instance +export default new AnalyticsService(); diff --git a/src/service/CampaignService.js b/src/service/CampaignService.js new file mode 100644 index 0000000..e042eb5 --- /dev/null +++ b/src/service/CampaignService.js @@ -0,0 +1,202 @@ +import { API_CONFIG, DEFAULT_REQUEST_CONFIG } from '@/config/api.js'; +import AuthService from '@/service/AuthService.js'; +import { CampaignDto } from '@/types/smm.js'; + +/** + * CampaignService - Управление кампаниями + * Базовый путь: /api/smm/campaigns + */ +class CampaignService { + constructor() { + this.baseUrl = API_CONFIG.SMM_BASE_URL; + this.endpoints = { + campaigns: API_CONFIG.ENDPOINTS.CAMPAIGNS, + campaignById: API_CONFIG.ENDPOINTS.CAMPAIGN_BY_ID + }; + } + + /** + * Получить все кампании + * GET /api/smm/campaigns + * @returns {Promise} + */ + async getAllCampaigns() { + try { + const response = await AuthService.authFetch(`${this.baseUrl}${this.endpoints.campaigns}`, { + method: 'GET', + headers: { + ...DEFAULT_REQUEST_CONFIG.headers + } + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + return data.map((campaign) => new CampaignDto(campaign)); + } catch (error) { + console.error('Error fetching campaigns:', error); + throw error; + } + } + + /** + * Получить кампанию по ID + * GET /api/smm/campaigns/{id} + * @param {string} id - UUID кампании + * @returns {Promise} + */ + async getCampaignById(id) { + try { + const response = await AuthService.authFetch(`${this.baseUrl}${this.endpoints.campaignById}/${id}`, { + method: 'GET', + headers: { + ...DEFAULT_REQUEST_CONFIG.headers + } + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + return new CampaignDto(data); + } catch (error) { + console.error(`Error fetching campaign ${id}:`, error); + throw error; + } + } + + /** + * Создать кампанию + * POST /api/smm/campaigns + * @param {Object} campaignData - Данные кампании + * @param {string} campaignData.name - Название кампании + * @param {string} campaignData.goal - Цель кампании + * @param {number} campaignData.budget - Бюджет кампании + * @param {string} campaignData.startAt - Дата начала (ISO 8601) + * @param {string} campaignData.endAt - Дата окончания (ISO 8601) + * @param {string} campaignData.status - Статус кампании + * @returns {Promise} + */ + async createCampaign(campaignData) { + try { + // Валидация + this.validateCampaignData(campaignData); + + const response = await AuthService.authFetch(`${this.baseUrl}${this.endpoints.campaigns}`, { + method: 'POST', + headers: { + ...DEFAULT_REQUEST_CONFIG.headers + }, + body: JSON.stringify(campaignData) + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(`HTTP error! status: ${response.status}, message: ${errorData.message || 'Unknown error'}`); + } + + const data = await response.json(); + return new CampaignDto(data); + } catch (error) { + console.error('Error creating campaign:', error); + throw error; + } + } + + /** + * Обновить кампанию + * PUT /api/smm/campaigns/{id} + * @param {string} id - UUID кампании + * @param {Object} campaignData - Данные кампании + * @returns {Promise} + */ + async updateCampaign(id, campaignData) { + try { + // Валидация + this.validateCampaignData(campaignData); + + const response = await AuthService.authFetch(`${this.baseUrl}${this.endpoints.campaignById}/${id}`, { + method: 'PUT', + headers: { + ...DEFAULT_REQUEST_CONFIG.headers + }, + body: JSON.stringify(campaignData) + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(`HTTP error! status: ${response.status}, message: ${errorData.message || 'Unknown error'}`); + } + + const data = await response.json(); + return new CampaignDto(data); + } catch (error) { + console.error(`Error updating campaign ${id}:`, error); + throw error; + } + } + + /** + * Удалить кампанию + * DELETE /api/smm/campaigns/{id} + * @param {string} id - UUID кампании + * @returns {Promise} + */ + async deleteCampaign(id) { + try { + const response = await AuthService.authFetch(`${this.baseUrl}${this.endpoints.campaignById}/${id}`, { + method: 'DELETE' + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + } catch (error) { + console.error(`Error deleting campaign ${id}:`, error); + throw error; + } + } + + /** + * Валидация данных кампании + * @param {Object} campaignData - Данные кампании + * @throws {Error} Если валидация не прошла + */ + validateCampaignData(campaignData) { + if (!campaignData.name || campaignData.name.trim() === '') { + throw new Error('Название кампании обязательно'); + } + + if (campaignData.budget !== undefined && campaignData.budget < 0) { + throw new Error('Бюджет должен быть >= 0'); + } + + if (campaignData.startAt) { + const startDate = new Date(campaignData.startAt); + if (isNaN(startDate.getTime())) { + throw new Error('Неверный формат даты начала'); + } + } + + if (campaignData.endAt) { + const endDate = new Date(campaignData.endAt); + if (isNaN(endDate.getTime())) { + throw new Error('Неверный формат даты окончания'); + } + } + + if (campaignData.startAt && campaignData.endAt) { + const startDate = new Date(campaignData.startAt); + const endDate = new Date(campaignData.endAt); + if (endDate <= startDate) { + throw new Error('Дата окончания должна быть позже даты начала'); + } + } + } +} + +// Экспорт singleton instance +export default new CampaignService(); diff --git a/src/service/ChannelService.js b/src/service/ChannelService.js new file mode 100644 index 0000000..9fcaf2c --- /dev/null +++ b/src/service/ChannelService.js @@ -0,0 +1,215 @@ +import { API_CONFIG, DEFAULT_REQUEST_CONFIG } from '@/config/api.js'; +import AuthService from '@/service/AuthService.js'; +import { ChannelDto, ChannelType } from '@/types/smm.js'; + +/** + * ChannelService - Управление каналами + * Базовый путь: /api/smm/channels + */ +class ChannelService { + constructor() { + this.baseUrl = API_CONFIG.SMM_BASE_URL; + this.endpoints = { + channels: API_CONFIG.ENDPOINTS.CHANNELS, + channelById: API_CONFIG.ENDPOINTS.CHANNEL_BY_ID + }; + } + + /** + * Получить все каналы + * GET /api/smm/channels + * @returns {Promise} + */ + async getAllChannels() { + try { + const response = await AuthService.authFetch(`${this.baseUrl}${this.endpoints.channels}`, { + method: 'GET', + headers: { + ...DEFAULT_REQUEST_CONFIG.headers + } + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + return data.map((channel) => new ChannelDto(channel)); + } catch (error) { + console.error('Error fetching channels:', error); + throw error; + } + } + + /** + * Получить канал по ID + * GET /api/smm/channels/{id} + * @param {string} id - UUID канала + * @returns {Promise} + */ + async getChannelById(id) { + try { + const response = await AuthService.authFetch(`${this.baseUrl}${this.endpoints.channelById}/${id}`, { + method: 'GET', + headers: { + ...DEFAULT_REQUEST_CONFIG.headers + } + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + return new ChannelDto(data); + } catch (error) { + console.error(`Error fetching channel ${id}:`, error); + throw error; + } + } + + /** + * Создать канал + * POST /api/smm/channels + * @param {Object} channelData - Данные канала + * @param {string} channelData.name - Название канала + * @param {string} channelData.type - Тип канала (TELEGRAM, VK, INSTAGRAM) + * @param {string} channelData.apiKeyRef - Ссылка на API ключ + * @param {boolean} channelData.isActive - Активен ли канал + * @returns {Promise} + */ + async createChannel(channelData) { + try { + // Валидация + this.validateChannelData(channelData); + + const response = await AuthService.authFetch(`${this.baseUrl}${this.endpoints.channels}`, { + method: 'POST', + headers: { + ...DEFAULT_REQUEST_CONFIG.headers + }, + body: JSON.stringify(channelData) + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(`HTTP error! status: ${response.status}, message: ${errorData.message || 'Unknown error'}`); + } + + const data = await response.json(); + return new ChannelDto(data); + } catch (error) { + console.error('Error creating channel:', error); + throw error; + } + } + + /** + * Обновить канал + * PUT /api/smm/channels/{id} + * @param {string} id - UUID канала + * @param {Object} channelData - Данные канала + * @returns {Promise} + */ + async updateChannel(id, channelData) { + try { + // Валидация + this.validateChannelData(channelData); + + const response = await AuthService.authFetch(`${this.baseUrl}${this.endpoints.channelById}/${id}`, { + method: 'PUT', + headers: { + ...DEFAULT_REQUEST_CONFIG.headers + }, + body: JSON.stringify(channelData) + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(`HTTP error! status: ${response.status}, message: ${errorData.message || 'Unknown error'}`); + } + + const data = await response.json(); + return new ChannelDto(data); + } catch (error) { + console.error(`Error updating channel ${id}:`, error); + throw error; + } + } + + /** + * Удалить канал + * DELETE /api/smm/channels/{id} + * @param {string} id - UUID канала + * @returns {Promise} + */ + async deleteChannel(id) { + try { + const response = await AuthService.authFetch(`${this.baseUrl}${this.endpoints.channelById}/${id}`, { + method: 'DELETE' + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + } catch (error) { + console.error(`Error deleting channel ${id}:`, error); + throw error; + } + } + + /** + * Получить каналы по типу + * @param {string} type - Тип канала + * @returns {Promise} + */ + async getChannelsByType(type) { + try { + const allChannels = await this.getAllChannels(); + return allChannels.filter((channel) => channel.type === type); + } catch (error) { + console.error(`Error fetching channels by type ${type}:`, error); + throw error; + } + } + + /** + * Получить активные каналы + * @returns {Promise} + */ + async getActiveChannels() { + try { + const allChannels = await this.getAllChannels(); + return allChannels.filter((channel) => channel.isActive); + } catch (error) { + console.error('Error fetching active channels:', error); + throw error; + } + } + + /** + * Валидация данных канала + * @param {Object} channelData - Данные канала + * @throws {Error} Если валидация не прошла + */ + validateChannelData(channelData) { + if (!channelData.name || channelData.name.trim() === '') { + throw new Error('Название канала обязательно'); + } + + if (!channelData.type || !Object.values(ChannelType).includes(channelData.type)) { + throw new Error('Тип канала обязателен и должен быть одним из: TELEGRAM, VK, INSTAGRAM'); + } + + if (!channelData.apiKeyRef || channelData.apiKeyRef.trim() === '') { + throw new Error('Ссылка на API ключ обязательна'); + } + + if (typeof channelData.isActive !== 'boolean') { + throw new Error('Поле isActive должно быть булевым значением'); + } + } +} + +// Экспорт singleton instance +export default new ChannelService(); diff --git a/src/service/ContentService.js b/src/service/ContentService.js new file mode 100644 index 0000000..3115150 --- /dev/null +++ b/src/service/ContentService.js @@ -0,0 +1,304 @@ +import { API_CONFIG, DEFAULT_REQUEST_CONFIG } from '@/config/api.js'; +import AuthService from '@/service/AuthService.js'; +import { ContentQueueDto, ContentStatus, MessageDto } from '@/types/smm.js'; + +/** + * ContentService - Управление контентом + * Базовый путь: /api/smm/content + */ +class ContentService { + constructor() { + this.baseUrl = API_CONFIG.SMM_BASE_URL; + this.endpoints = { + content: API_CONFIG.ENDPOINTS.CONTENT, + contentById: API_CONFIG.ENDPOINTS.CONTENT_BY_ID, + contentApprove: API_CONFIG.ENDPOINTS.CONTENT_APPROVE, + contentMessages: API_CONFIG.ENDPOINTS.CONTENT_MESSAGES + }; + } + + /** + * Получить весь контент + * GET /api/smm/content + * @returns {Promise} + */ + async getAllContent() { + try { + const response = await AuthService.authFetch(`${this.baseUrl}${this.endpoints.content}`, { + method: 'GET', + headers: { + ...DEFAULT_REQUEST_CONFIG.headers + } + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + return data.map((content) => new ContentQueueDto(content)); + } catch (error) { + console.error('Error fetching content:', error); + throw error; + } + } + + /** + * Получить контент по ID + * GET /api/smm/content/{id} + * @param {string} id - UUID контента + * @returns {Promise} + */ + async getContentById(id) { + try { + const response = await AuthService.authFetch(`${this.baseUrl}${this.endpoints.contentById}/${id}`, { + method: 'GET', + headers: { + ...DEFAULT_REQUEST_CONFIG.headers + } + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + return new ContentQueueDto(data); + } catch (error) { + console.error(`Error fetching content ${id}:`, error); + throw error; + } + } + + /** + * Создать контент + * POST /api/smm/content + * @param {Object} contentData - Данные контента + * @param {string} contentData.campaignId - ID кампании + * @param {string} contentData.locale - Локаль + * @param {string} contentData.topic - Тема + * @param {string} contentData.postDraft - Черновик поста + * @param {string} contentData.assetsRefs - Ссылки на ресурсы + * @param {string} contentData.scheduledAt - Время публикации (ISO 8601) + * @param {number} contentData.priority - Приоритет + * @returns {Promise} + */ + async createContent(contentData) { + try { + // Валидация + this.validateContentData(contentData); + + const response = await AuthService.authFetch(`${this.baseUrl}${this.endpoints.content}`, { + method: 'POST', + headers: { + ...DEFAULT_REQUEST_CONFIG.headers + }, + body: JSON.stringify(contentData) + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(`HTTP error! status: ${response.status}, message: ${errorData.message || 'Unknown error'}`); + } + + const data = await response.json(); + return new ContentQueueDto(data); + } catch (error) { + console.error('Error creating content:', error); + throw error; + } + } + + /** + * Обновить контент + * PUT /api/smm/content/{id} + * @param {string} id - UUID контента + * @param {Object} contentData - Данные контента + * @returns {Promise} + */ + async updateContent(id, contentData) { + try { + // Валидация + this.validateContentData(contentData); + + const response = await AuthService.authFetch(`${this.baseUrl}${this.endpoints.contentById}/${id}`, { + method: 'PUT', + headers: { + ...DEFAULT_REQUEST_CONFIG.headers + }, + body: JSON.stringify(contentData) + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(`HTTP error! status: ${response.status}, message: ${errorData.message || 'Unknown error'}`); + } + + const data = await response.json(); + return new ContentQueueDto(data); + } catch (error) { + console.error(`Error updating content ${id}:`, error); + throw error; + } + } + + /** + * Удалить контент + * DELETE /api/smm/content/{id} + * @param {string} id - UUID контента + * @returns {Promise} + */ + async deleteContent(id) { + try { + const response = await AuthService.authFetch(`${this.baseUrl}${this.endpoints.contentById}/${id}`, { + method: 'DELETE' + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + } catch (error) { + console.error(`Error deleting content ${id}:`, error); + throw error; + } + } + + /** + * Одобрить контент + * POST /api/smm/content/{id}/approve + * @param {string} id - UUID контента + * @returns {Promise} + */ + async approveContent(id) { + try { + const response = await AuthService.authFetch(`${this.baseUrl}${this.endpoints.contentApprove}/${id}/approve`, { + method: 'POST' + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(`HTTP error! status: ${response.status}, message: ${errorData.message || 'Unknown error'}`); + } + + const data = await response.json(); + return new ContentQueueDto(data); + } catch (error) { + console.error(`Error approving content ${id}:`, error); + throw error; + } + } + + /** + * Получить сообщения контента + * GET /api/smm/content/{id}/messages + * @param {string} id - UUID контента + * @returns {Promise} + */ + async getContentMessages(id) { + try { + const response = await AuthService.authFetch(`${this.baseUrl}${this.endpoints.contentMessages}/${id}/messages`, { + method: 'GET', + headers: { + ...DEFAULT_REQUEST_CONFIG.headers + } + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + return data.map((message) => new MessageDto(message)); + } catch (error) { + console.error(`Error fetching content messages ${id}:`, error); + throw error; + } + } + + /** + * Получить контент по кампании + * @param {string} campaignId - UUID кампании + * @returns {Promise} + */ + async getContentByCampaign(campaignId) { + try { + const allContent = await this.getAllContent(); + return allContent.filter((content) => content.campaignId === campaignId); + } catch (error) { + console.error(`Error fetching content by campaign ${campaignId}:`, error); + throw error; + } + } + + /** + * Получить контент по статусу + * @param {string} status - Статус контента + * @returns {Promise} + */ + async getContentByStatus(status) { + try { + const allContent = await this.getAllContent(); + return allContent.filter((content) => content.status === status); + } catch (error) { + console.error(`Error fetching content by status ${status}:`, error); + throw error; + } + } + + /** + * Получить контент, ожидающий одобрения + * @returns {Promise} + */ + async getPendingApprovalContent() { + return this.getContentByStatus(ContentStatus.PENDING_APPROVAL); + } + + /** + * Получить одобренный контент + * @returns {Promise} + */ + async getApprovedContent() { + return this.getContentByStatus(ContentStatus.APPROVED); + } + + /** + * Получить опубликованный контент + * @returns {Promise} + */ + async getPublishedContent() { + return this.getContentByStatus(ContentStatus.PUBLISHED); + } + + /** + * Валидация данных контента + * @param {Object} contentData - Данные контента + * @throws {Error} Если валидация не прошла + */ + validateContentData(contentData) { + if (!contentData.campaignId || contentData.campaignId.trim() === '') { + throw new Error('ID кампании обязателен'); + } + + if (!contentData.locale || contentData.locale.trim() === '') { + throw new Error('Локаль обязательна'); + } + + if (!contentData.topic || contentData.topic.trim() === '') { + throw new Error('Тема обязательна'); + } + + if (contentData.scheduledAt) { + const scheduledDate = new Date(contentData.scheduledAt); + if (isNaN(scheduledDate.getTime())) { + throw new Error('Неверный формат времени публикации'); + } + } + + if (contentData.priority !== undefined && (contentData.priority < 1 || contentData.priority > 10)) { + throw new Error('Приоритет должен быть от 1 до 10'); + } + } +} + +// Экспорт singleton instance +export default new ContentService(); diff --git a/src/service/PublishingService.js b/src/service/PublishingService.js new file mode 100644 index 0000000..3d12c2b --- /dev/null +++ b/src/service/PublishingService.js @@ -0,0 +1,153 @@ +import { API_CONFIG, DEFAULT_REQUEST_CONFIG } from '@/config/api.js'; +import AuthService from '@/service/AuthService.js'; +import { PublishingResultDto } from '@/types/smm.js'; + +/** + * PublishingService - Публикация контента + * Базовый путь: /api/smm/publishing + */ +class PublishingService { + constructor() { + this.baseUrl = API_CONFIG.SMM_BASE_URL; + this.endpoints = { + publishPost: API_CONFIG.ENDPOINTS.PUBLISHING_POST + }; + } + + /** + * Опубликовать контент + * POST /api/smm/publishing/post/{contentId} + * @param {string} contentId - UUID контента для публикации + * @returns {Promise} + */ + async publishContent(contentId) { + try { + if (!contentId || contentId.trim() === '') { + throw new Error('ID контента обязателен'); + } + + const response = await AuthService.authFetch(`${this.baseUrl}${this.endpoints.publishPost}/${contentId}`, { + method: 'POST', + headers: { + ...DEFAULT_REQUEST_CONFIG.headers + } + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(`HTTP error! status: ${response.status}, message: ${errorData.message || 'Unknown error'}`); + } + + const data = await response.json(); + return new PublishingResultDto(data); + } catch (error) { + console.error(`Error publishing content ${contentId}:`, error); + throw error; + } + } + + /** + * Проверить статус публикации контента + * @param {string} contentId - UUID контента + * @returns {Promise} - true если контент опубликован + */ + async isContentPublished(contentId) { + try { + // Импортируем ContentService для проверки статуса + const { default: ContentService } = await import('./ContentService.js'); + const content = await ContentService.getContentById(contentId); + return content.status === 'PUBLISHED'; + } catch (error) { + console.error(`Error checking publication status for content ${contentId}:`, error); + return false; + } + } + + /** + * Получить URL опубликованного контента + * @param {string} contentId - UUID контента + * @returns {Promise} - URL публикации или null + */ + async getPublishedContentUrl(contentId) { + try { + // Импортируем ContentService для получения сообщений + const { default: ContentService } = await import('./ContentService.js'); + const messages = await ContentService.getContentMessages(contentId); + + if (messages && messages.length > 0) { + // Возвращаем URL первого сообщения + return messages[0].url; + } + + return null; + } catch (error) { + console.error(`Error getting published content URL for ${contentId}:`, error); + return null; + } + } + + /** + * Получить статистику публикаций + * @param {string} contentId - UUID контента + * @returns {Promise} - Статистика публикаций + */ + async getPublishingStats(contentId) { + try { + // Импортируем ContentService для получения сообщений + const { default: ContentService } = await import('./ContentService.js'); + const messages = await ContentService.getContentMessages(contentId); + + return { + totalMessages: messages.length, + publishedChannels: messages.map((msg) => msg.channelId), + urls: messages.map((msg) => msg.url), + lastPublished: messages.length > 0 ? new Date(Math.max(...messages.map((msg) => new Date(msg.postedAt).getTime()))) : null + }; + } catch (error) { + console.error(`Error getting publishing stats for content ${contentId}:`, error); + return { + totalMessages: 0, + publishedChannels: [], + urls: [], + lastPublished: null + }; + } + } + + /** + * Массовая публикация контента + * @param {string[]} contentIds - Массив UUID контента + * @returns {Promise} - Результаты публикации + */ + async publishMultipleContent(contentIds) { + try { + if (!Array.isArray(contentIds) || contentIds.length === 0) { + throw new Error('Массив ID контента не может быть пустым'); + } + + const results = []; + + for (const contentId of contentIds) { + try { + const result = await this.publishContent(contentId); + results.push(result); + } catch (error) { + console.error(`Error publishing content ${contentId}:`, error); + results.push({ + success: false, + message: `Ошибка публикации контента ${contentId}: ${error.message}`, + data: null + }); + } + } + + return results; + } catch (error) { + console.error('Error in mass publishing:', error); + throw error; + } + } +} + +// Экспорт singleton instance +export default new PublishingService(); diff --git a/src/types/smm.js b/src/types/smm.js new file mode 100644 index 0000000..4a52fa6 --- /dev/null +++ b/src/types/smm.js @@ -0,0 +1,103 @@ +// SMM System Types and Enums + +// Campaign Status Enum +export const CampaignStatus = { + PLANNED: 'PLANNED', + ACTIVE: 'ACTIVE', + COMPLETED: 'COMPLETED' +}; + +// Channel Type Enum +export const ChannelType = { + TELEGRAM: 'TELEGRAM', + VK: 'VK', + INSTAGRAM: 'INSTAGRAM' +}; + +// Content Status Enum +export const ContentStatus = { + DRAFT: 'DRAFT', + PENDING_APPROVAL: 'PENDING_APPROVAL', + APPROVED: 'APPROVED', + PUBLISHED: 'PUBLISHED', + FAILED: 'FAILED' +}; + +// Campaign DTO +export class CampaignDto { + constructor(data = {}) { + this.id = data.id; + this.name = data.name; + this.goal = data.goal; + this.budget = data.budget; + this.startAt = data.startAt; + this.endAt = data.endAt; + this.status = data.status; + } +} + +// Channel DTO +export class ChannelDto { + constructor(data = {}) { + this.id = data.id; + this.name = data.name; + this.type = data.type; + this.apiKeyRef = data.apiKeyRef; + this.isActive = data.isActive; + } +} + +// Content Queue DTO +export class ContentQueueDto { + constructor(data = {}) { + this.id = data.id; + this.campaignId = data.campaignId; + this.locale = data.locale; + this.topic = data.topic; + this.postDraft = data.postDraft; + this.assetsRefs = data.assetsRefs; + this.scheduledAt = data.scheduledAt; + this.priority = data.priority; + this.status = data.status; + } +} + +// Message DTO +export class MessageDto { + constructor(data = {}) { + this.id = data.id; + this.channelId = data.channelId; + this.contentId = data.contentId; + this.externalId = data.externalId; + this.url = data.url; + this.postedAt = data.postedAt; + } +} + +// Publishing Result DTO +export class PublishingResultDto { + constructor(data = {}) { + this.success = data.success; + this.message = data.message; + this.data = data.data; + } +} + +// Analytics Result DTO +export class AnalyticsResultDto { + constructor(data = {}) { + this.success = data.success; + this.message = data.message; + } +} + +// Error Response DTO +export class ErrorResponseDto { + constructor(data = {}) { + this.timestamp = data.timestamp; + this.status = data.status; + this.error = data.error; + this.message = data.message; + this.path = data.path; + } +} diff --git a/src/views/smm/AnalyticsDashboard.vue b/src/views/smm/AnalyticsDashboard.vue new file mode 100644 index 0000000..d964d2f --- /dev/null +++ b/src/views/smm/AnalyticsDashboard.vue @@ -0,0 +1,404 @@ + + + + + diff --git a/src/views/smm/CampaignList.vue b/src/views/smm/CampaignList.vue new file mode 100644 index 0000000..27d4a81 --- /dev/null +++ b/src/views/smm/CampaignList.vue @@ -0,0 +1,399 @@ +