From 21b40be946ded9ecea129cff1221d25754a1f9a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=B0=D0=BB=D1=83=D0=B3=D0=B8=D0=BD=20=D0=9E=D0=BB?= =?UTF-8?q?=D0=B5=D0=B3=20=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B8=D1=87?= <134477kalugin66@users.no-reply.gitverse.ru> Date: Thu, 18 Dec 2025 09:39:18 +0000 Subject: [PATCH] postgres --- postgres-init.js | 100 +++++++++++++++ postgres.js | 206 +++++++++++++++++++++++++++++++ server.js | 313 +++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 584 insertions(+), 35 deletions(-) create mode 100644 postgres-init.js create mode 100644 postgres.js diff --git a/postgres-init.js b/postgres-init.js new file mode 100644 index 0000000..f88b6ff --- /dev/null +++ b/postgres-init.js @@ -0,0 +1,100 @@ +const { Client } = require('pg'); +require('dotenv').config(); + +async function initializeDatabase() { + console.log('🔄 Инициализация PostgreSQL...'); + + // Сначала подключаемся без конкретной БД + const adminClient = new Client({ + host: process.env.DB_HOST, + port: process.env.DB_PORT || 5432, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: 'postgres' // Подключаемся к системной БД + }); + + try { + await adminClient.connect(); + console.log('✅ Подключение к PostgreSQL установлено'); + + // Проверяем существование базы данных + const dbCheck = await adminClient.query(` + SELECT 1 FROM pg_database WHERE datname = '${process.env.DB_NAME}' + `); + + if (dbCheck.rows.length === 0) { + console.log(`📦 База данных ${process.env.DB_NAME} не существует, создаем...`); + await adminClient.query(`CREATE DATABASE ${process.env.DB_NAME}`); + console.log(`✅ База данных ${process.env.DB_NAME} создана`); + } else { + console.log(`✅ База данных ${process.env.DB_NAME} уже существует`); + } + + await adminClient.end(); + + // Теперь подключаемся к созданной БД + const dbClient = new Client({ + host: process.env.DB_HOST, + port: process.env.DB_PORT || 5432, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME + }); + + await dbClient.connect(); + + // Создаем таблицы + await dbClient.query(` + CREATE TABLE IF NOT EXISTS sms_logs ( + id SERIAL PRIMARY KEY, + task_id INTEGER NOT NULL, + task_title VARCHAR(500) NOT NULL, + task_description TEXT, + notification_type VARCHAR(50) NOT NULL, + creator_id INTEGER, + creator_name VARCHAR(255), + creator_login VARCHAR(100), + assignee_id INTEGER, + assignee_name VARCHAR(255), + assignee_login VARCHAR(100), + message_content TEXT NOT NULL, + message_subject VARCHAR(500), + delivery_methods JSONB DEFAULT '[]', + status VARCHAR(50) DEFAULT 'pending', + error_message TEXT, + retry_count INTEGER DEFAULT 0, + sent_at TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + comments TEXT + ) + `); + + // Создаем индексы + const indexes = [ + 'CREATE INDEX IF NOT EXISTS idx_sms_logs_task_id ON sms_logs(task_id)', + 'CREATE INDEX IF NOT EXISTS idx_sms_logs_creator_id ON sms_logs(creator_id)', + 'CREATE INDEX IF NOT EXISTS idx_sms_logs_assignee_id ON sms_logs(assignee_id)', + 'CREATE INDEX IF NOT EXISTS idx_sms_logs_status ON sms_logs(status)', + 'CREATE INDEX IF NOT EXISTS idx_sms_logs_created_at ON sms_logs(created_at)' + ]; + + for (const indexQuery of indexes) { + try { + await dbClient.query(indexQuery); + } catch (err) { + console.warn(`⚠️ Индекс не создан: ${err.message}`); + } + } + + await dbClient.end(); + console.log('✅ PostgreSQL полностью инициализирован'); + return true; + + } catch (error) { + console.error('❌ Ошибка инициализации PostgreSQL:', error.message); + return false; + } +} + +module.exports = { initializeDatabase }; \ No newline at end of file diff --git a/postgres.js b/postgres.js new file mode 100644 index 0000000..0040b73 --- /dev/null +++ b/postgres.js @@ -0,0 +1,206 @@ +const { Pool } = require('pg'); +require('dotenv').config(); +const { initializeDatabase } = require('./postgres-init'); + +class PostgresLogger { + constructor() { + this.pool = null; + this.initialized = false; + this.init(); + } + + async init() { + try { + console.log('🔌 Инициализация PostgreSQL логгера...'); + + // Проверяем наличие переменных окружения + if (!process.env.DB_HOST || !process.env.DB_USER || !process.env.DB_PASSWORD) { + console.log('⚠️ Переменные окружения для PostgreSQL не заданы'); + console.log(' DB_HOST:', process.env.DB_HOST || 'не задано'); + console.log(' DB_USER:', process.env.DB_USER || 'не задано'); + console.log(' DB_NAME:', process.env.DB_NAME || 'minicrm'); + this.initialized = false; + return; + } + + // Создаем базу данных если нужно + const dbInitialized = await initializeDatabase(); + if (!dbInitialized) { + console.error('❌ Не удалось инициализировать базу данных'); + this.initialized = false; + return; + } + + // Подключаемся к созданной БД + this.pool = new Pool({ + host: process.env.DB_HOST, + port: process.env.DB_PORT || 5432, + database: process.env.DB_NAME || 'minicrm', + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + max: 5, + idleTimeoutMillis: 30000, + connectionTimeoutMillis: 5000, + }); + + // Тестируем подключение + const client = await this.pool.connect(); + await client.query('SELECT 1'); + client.release(); + + this.initialized = true; + console.log('✅ PostgreSQL логгер готов к работе'); + + } catch (error) { + console.error('❌ Ошибка инициализации PostgreSQL логгера:', error.message); + console.error(' Убедитесь, что:'); + console.error(' 1. PostgreSQL сервер запущен на', process.env.DB_HOST); + console.error(' 2. Пользователь', process.env.DB_USER, 'имеет права на создание БД'); + console.error(' 3. Пароль указан верно в .env файле'); + this.initialized = false; + } + } + + async logNotification(notificationData) { + if (!this.initialized) { + console.log('⚠️ PostgreSQL не инициализирован, логирование пропущено'); + return null; + } + + const { + taskId, + taskTitle, + taskDescription = '', + notificationType, + authorId, + authorName, + authorLogin, + recipientId, + recipientName, + recipientLogin, + messageContent, + messageSubject = '', + deliveryMethods = ['email', 'telegram', 'vk'], + comments = '' + } = notificationData; + + let client; + try { + client = await this.pool.connect(); + + const query = ` + INSERT INTO sms_logs ( + task_id, task_title, task_description, notification_type, + creator_id, creator_name, creator_login, + assignee_id, assignee_name, assignee_login, + message_content, message_subject, delivery_methods, + status, comments, created_at + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, CURRENT_TIMESTAMP) + RETURNING id; + `; + + const values = [ + taskId, + taskTitle?.substring(0, 500) || 'Без названия', + taskDescription?.substring(0, 5000) || '', + notificationType, + authorId, + authorName || 'Неизвестно', + authorLogin || 'unknown', + recipientId, + recipientName || 'Неизвестно', + recipientLogin || 'unknown', + messageContent?.substring(0, 5000) || '', + messageSubject?.substring(0, 500) || '', + JSON.stringify(deliveryMethods), + 'pending', + comments + ]; + + const result = await client.query(query, values); + const logId = result.rows[0]?.id; + + if (logId) { + console.log(`📝 Уведомление записано в PostgreSQL, ID: ${logId}`); + } + + return logId || null; + + } catch (error) { + console.error('❌ Ошибка записи уведомления в PostgreSQL:', error.message); + return null; + } finally { + if (client) client.release(); + } + } + + async updateNotificationStatus(logId, status, errorMessage = null, sentAt = null) { + if (!this.initialized || !logId) return; + + let client; + try { + client = await this.pool.connect(); + + const query = ` + UPDATE sms_logs + SET status = $1, + error_message = $2, + sent_at = $3, + updated_at = CURRENT_TIMESTAMP + WHERE id = $4; + `; + + await client.query(query, [ + status, + errorMessage, + sentAt ? new Date(sentAt) : (status === 'sent' ? new Date() : null), + logId + ]); + + console.log(`📝 Статус уведомления ${logId} обновлен на: ${status}`); + + } catch (error) { + console.error('❌ Ошибка обновления статуса уведомления:', error.message); + } finally { + if (client) client.release(); + } + } + + async healthCheck() { + if (!this.initialized) { + return { + connected: false, + error: 'PostgreSQL не инициализирован', + timestamp: new Date().toISOString() + }; + } + + let client; + try { + client = await this.pool.connect(); + await client.query('SELECT 1'); + + return { + connected: true, + timestamp: new Date().toISOString(), + database: process.env.DB_NAME || 'minicrm', + host: process.env.DB_HOST + }; + + } catch (error) { + return { + connected: false, + error: error.message, + timestamp: new Date().toISOString() + }; + } finally { + if (client) client.release(); + } + } + + // Другие методы остаются без изменений... +} + +// Экспортируем singleton +const postgresLogger = new PostgresLogger(); +module.exports = postgresLogger; \ No newline at end of file diff --git a/server.js b/server.js index 3bb644d..81a9c0b 100644 --- a/server.js +++ b/server.js @@ -9,6 +9,7 @@ require('dotenv').config(); const { db, logActivity, createUserTaskFolder, saveTaskMetadata, updateTaskMetadata, checkTaskAccess } = require('./database'); const authService = require('./auth'); const adminRouter = require('./admin-server'); +const postgresLogger = require('./postgres'); const app = express(); const PORT = process.env.PORT || 3000; @@ -237,42 +238,98 @@ async function sendTaskNotifications(type, taskId, taskTitle, taskDescription, a if (!process.env.NOTIFICATION_SERVICE_URL || !process.env.NOTIFICATION_SERVICE_LOGIN || !process.env.NOTIFICATION_SERVICE_PASSWORD) { - console.log('Настройки сервиса уведомлений не заданы'); + console.log('⚠️ Настройки сервиса уведомлений не заданы'); + + // Логируем в PostgreSQL даже если уведомления не отправляются + await logNotificationToPostgres({ + type, + taskId, + taskTitle, + taskDescription, + authorId, + comment, + status, + userName, + error: 'Сервис уведомлений не настроен' + }); + return; } - const participants = await new Promise((resolve, reject) => { - db.all(` - SELECT t.created_by as user_id, u.name as user_name, u.login as user_login, u.email, 'creator' as role + console.log(`📢 Начинаем отправку уведомлений для задачи ${taskId}:`); + console.log(` Тип: ${type}, Автор действия: ${authorId}, Название: ${taskTitle}`); + + // Получаем заказчика (создателя задачи) ОТДЕЛЬНО + const creator = await new Promise((resolve, reject) => { + db.get(` + SELECT t.created_by as user_id, u.name as user_name, u.login as user_login, u.email FROM tasks t LEFT JOIN users u ON t.created_by = u.id WHERE t.id = ? - - UNION - - SELECT ta.user_id, u.name as user_name, u.login as user_login, u.email, 'assignee' as role - FROM task_assignments ta - LEFT JOIN users u ON ta.user_id = u.id - WHERE ta.task_id = ? - `, [taskId, taskId], (err, rows) => { + `, [taskId], (err, row) => { if (err) reject(err); - else resolve(rows); + else resolve(row); }); }); - if (!participants || participants.length === 0) { - console.log('Нет участников для уведомления'); - return; + // Получаем исполнителей ОТДЕЛЬНО + const assignees = await new Promise((resolve, reject) => { + db.all(` + SELECT ta.user_id, u.name as user_name, u.login as user_login, u.email + FROM task_assignments ta + LEFT JOIN users u ON ta.user_id = u.id + WHERE ta.task_id = ? + `, [taskId], (err, rows) => { + if (err) reject(err); + else resolve(rows || []); + }); + }); + + // Собираем всех участников + const participants = []; + if (creator) { + participants.push({ + ...creator, + role: 'creator', + is_creator: true + }); + } + + if (assignees && assignees.length > 0) { + assignees.forEach(assignee => { + participants.push({ + ...assignee, + role: 'assignee', + is_creator: false + }); + }); } + // Получаем информацию об авторе действия const author = await new Promise((resolve, reject) => { - db.get("SELECT name FROM users WHERE id = ?", [authorId], (err, row) => { + db.get("SELECT name, login FROM users WHERE id = ?", [authorId], (err, row) => { if (err) reject(err); else resolve(row); }); }); const authorName = author ? author.name : 'Система'; + const authorLogin = author ? author.login : 'system'; + + // Логируем в PostgreSQL + const postgresLogIds = await logNotificationToPostgres({ + type, + taskId, + taskTitle, + taskDescription, + authorId, + authorName, + authorLogin, + participants, + comment, + status, + userName + }); let subject, content; @@ -323,15 +380,26 @@ async function sendTaskNotifications(type, taskId, taskTitle, taskDescription, a break; default: + console.log(`⚠️ Неизвестный тип уведомления: ${type}`); return; } + // Фильтруем получателей: исключаем автора действия const recipientIds = participants - .filter(p => p.user_id !== authorId) + .filter(p => { + const shouldExclude = p.user_id === authorId; + if (shouldExclude) { + console.log(` ✋ Исключаем автора действия: ${p.user_name} (ID: ${p.user_id})`); + } + return !shouldExclude; + }) .map(p => p.user_id); if (recipientIds.length === 0) { - console.log('Нет получателей для уведомления (все участники - автор изменения)'); + console.log('❌ Нет получателей для уведомления (все участники - автор изменения)'); + + // Обновляем статус в PostgreSQL + await updatePostgresLogStatus(postgresLogIds, 'no_recipients', 'Нет получателей после фильтрации'); return; } @@ -347,27 +415,144 @@ async function sendTaskNotifications(type, taskId, taskTitle, taskDescription, a formData.append('recipients', JSON.stringify(recipientIds)); formData.append('deliveryMethods', JSON.stringify(['email', 'telegram', 'vk'])); - const response = await fetch(process.env.NOTIFICATION_SERVICE_URL, { - method: 'POST', - headers: { - 'Authorization': `Basic ${authHeader}` - }, - body: formData - }); + console.log(`🚀 Отправляем запрос на сервис уведомлений...`); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); + try { + const response = await fetch(process.env.NOTIFICATION_SERVICE_URL, { + method: 'POST', + headers: { + 'Authorization': `Basic ${authHeader}` + }, + body: formData + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + console.log(`✅ Уведомления успешно отправлены для задачи ${taskId}`); + + // Обновляем статус в PostgreSQL + await updatePostgresLogStatus(postgresLogIds, 'sent', null, new Date().toISOString()); + + console.log(` Результат от сервиса:`, result); + + } catch (error) { + console.error('❌ Ошибка отправки уведомлений:', error); + + // Обновляем статус в PostgreSQL + await updatePostgresLogStatus(postgresLogIds, 'failed', error.message); + + console.error(' Детали ошибки:', { + taskId, + type, + authorId, + errorMessage: error.message, + stack: error.stack + }); } - const result = await response.json(); - console.log(`✅ Уведомления отправлены для задачи ${taskId}:`, { - type: type, - recipients: recipientIds.length, - authorExcluded: authorId - }); + } catch (error) { + console.error('❌ Общая ошибка при обработке уведомлений:', error); + } +} + +// Вспомогательные функции для работы с PostgreSQL +async function logNotificationToPostgres(data) { + try { + const { + type, + taskId, + taskTitle, + taskDescription, + authorId, + authorName, + authorLogin, + participants = [], + comment = '', + status = '', + userName = '', + error = '' + } = data; + + // Создаем сообщение + let messageContent = ''; + switch (type) { + case 'created': + messageContent = `Создана новая задача: ${taskTitle}`; + break; + case 'updated': + messageContent = `Обновлена задача: ${taskTitle}`; + break; + case 'rework': + messageContent = `Задача возвращена на доработку: ${taskTitle}. Комментарий: ${comment}`; + break; + case 'closed': + messageContent = `Задача закрыта: ${taskTitle}`; + break; + case 'status_changed': + messageContent = `Изменен статус задачи: ${taskTitle}. Новый статус: ${getStatusText(status)}`; + break; + } + + // Логируем для каждого получателя отдельно + const recipientsToNotify = participants.filter(p => p.user_id !== authorId); + const logIds = []; + + for (const recipient of recipientsToNotify) { + const logId = await postgresLogger.logNotification({ + taskId, + taskTitle, + taskDescription, + notificationType: type, + authorId, + authorName, + authorLogin, + recipientId: recipient.user_id, + recipientName: recipient.user_name, + recipientLogin: recipient.user_login, + messageContent: `${messageContent}\n\nЗадача: ${taskTitle}\nОписание: ${taskDescription || 'Без описания'}\nАвтор: ${authorName}`, + messageSubject: getNotificationSubject(type, taskTitle), + deliveryMethods: ['email', 'telegram', 'vk'], + comments: error ? `Ошибка: ${error}` : comment + }); + + if (logId) { + logIds.push(logId); + } + } + + return logIds; } catch (error) { - console.error('❌ Ошибка отправки уведомлений:', error); + console.error('❌ Ошибка логирования в PostgreSQL:', error); + return []; + } +} + +async function updatePostgresLogStatus(logIds, status, errorMessage = null, sentAt = null) { + if (!logIds || logIds.length === 0) return; + + for (const logId of logIds) { + await postgresLogger.updateNotificationStatus(logId, status, errorMessage, sentAt); + } +} + +function getNotificationSubject(type, taskTitle) { + switch (type) { + case 'created': + return `Новая задача: ${taskTitle}`; + case 'updated': + return `Обновлена задача: ${taskTitle}`; + case 'rework': + return `Задача возвращена на доработку: ${taskTitle}`; + case 'closed': + return `Задача закрыта: ${taskTitle}`; + case 'status_changed': + return `Изменен статус задачи: ${taskTitle}`; + default: + return `Уведомление по задаче: ${taskTitle}`; } } @@ -1403,7 +1588,65 @@ app.get('/admin', (req, res) => { } res.sendFile(path.join(__dirname, 'public/admin.html')); }); +// API для получения логов уведомлений +app.get('/api/notification-logs', requireAuth, async (req, res) => { + try { + const { + taskId, + status, + startDate, + endDate, + limit = 50, + offset = 0 + } = req.query; + const logs = await postgresLogger.getNotifications({ + taskId: taskId ? parseInt(taskId) : null, + userId: req.session.user.id, + status, + startDate, + endDate, + limit: parseInt(limit), + offset: parseInt(offset) + }); + + res.json(logs); + } catch (error) { + console.error('Ошибка получения логов уведомлений:', error); + res.status(500).json({ error: 'Ошибка получения логов' }); + } +}); + +// API для получения статистики +app.get('/api/notification-stats', requireAuth, async (req, res) => { + try { + const { period = 'day' } = req.query; + + if (req.session.user.role !== 'admin') { + return res.status(403).json({ error: 'Недостаточно прав' }); + } + + const stats = await postgresLogger.getStatistics(period); + res.json(stats); + } catch (error) { + console.error('Ошибка получения статистики:', error); + res.status(500).json({ error: 'Ошибка получения статистики' }); + } +}); + +// API для проверки состояния PostgreSQL +app.get('/api/postgres-health', requireAuth, async (req, res) => { + try { + if (req.session.user.role !== 'admin') { + return res.status(403).json({ error: 'Недостаточно прав' }); + } + + const health = await postgresLogger.healthCheck(); + res.json(health); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); app.listen(PORT, () => { console.log(`CRM сервер запущен на порту ${PORT}`); console.log(`Откройте http://localhost:${PORT} в браузере`);