This commit is contained in:
Калугин Олег Александрович
2025-12-18 09:39:18 +00:00
parent 540fcca315
commit 21b40be946
3 changed files with 584 additions and 35 deletions

100
postgres-init.js Normal file
View File

@@ -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 };

206
postgres.js Normal file
View File

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

313
server.js
View File

@@ -9,6 +9,7 @@ require('dotenv').config();
const { db, logActivity, createUserTaskFolder, saveTaskMetadata, updateTaskMetadata, checkTaskAccess } = require('./database'); const { db, logActivity, createUserTaskFolder, saveTaskMetadata, updateTaskMetadata, checkTaskAccess } = require('./database');
const authService = require('./auth'); const authService = require('./auth');
const adminRouter = require('./admin-server'); const adminRouter = require('./admin-server');
const postgresLogger = require('./postgres');
const app = express(); const app = express();
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
@@ -237,42 +238,98 @@ async function sendTaskNotifications(type, taskId, taskTitle, taskDescription, a
if (!process.env.NOTIFICATION_SERVICE_URL || if (!process.env.NOTIFICATION_SERVICE_URL ||
!process.env.NOTIFICATION_SERVICE_LOGIN || !process.env.NOTIFICATION_SERVICE_LOGIN ||
!process.env.NOTIFICATION_SERVICE_PASSWORD) { !process.env.NOTIFICATION_SERVICE_PASSWORD) {
console.log('Настройки сервиса уведомлений не заданы'); console.log('⚠️ Настройки сервиса уведомлений не заданы');
// Логируем в PostgreSQL даже если уведомления не отправляются
await logNotificationToPostgres({
type,
taskId,
taskTitle,
taskDescription,
authorId,
comment,
status,
userName,
error: 'Сервис уведомлений не настроен'
});
return; return;
} }
const participants = await new Promise((resolve, reject) => { console.log(`📢 Начинаем отправку уведомлений для задачи ${taskId}:`);
db.all(` console.log(` Тип: ${type}, Автор действия: ${authorId}, Название: ${taskTitle}`);
SELECT t.created_by as user_id, u.name as user_name, u.login as user_login, u.email, 'creator' as role
// Получаем заказчика (создателя задачи) ОТДЕЛЬНО
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 FROM tasks t
LEFT JOIN users u ON t.created_by = u.id LEFT JOIN users u ON t.created_by = u.id
WHERE t.id = ? WHERE t.id = ?
`, [taskId], (err, row) => {
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) => {
if (err) reject(err); if (err) reject(err);
else resolve(rows); else resolve(row);
}); });
}); });
if (!participants || participants.length === 0) { // Получаем исполнителей ОТДЕЛЬНО
console.log('Нет участников для уведомления'); const assignees = await new Promise((resolve, reject) => {
return; 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) => { 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); if (err) reject(err);
else resolve(row); else resolve(row);
}); });
}); });
const authorName = author ? author.name : 'Система'; 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; let subject, content;
@@ -323,15 +380,26 @@ async function sendTaskNotifications(type, taskId, taskTitle, taskDescription, a
break; break;
default: default:
console.log(`⚠️ Неизвестный тип уведомления: ${type}`);
return; return;
} }
// Фильтруем получателей: исключаем автора действия
const recipientIds = participants 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); .map(p => p.user_id);
if (recipientIds.length === 0) { if (recipientIds.length === 0) {
console.log('Нет получателей для уведомления (все участники - автор изменения)'); console.log('Нет получателей для уведомления (все участники - автор изменения)');
// Обновляем статус в PostgreSQL
await updatePostgresLogStatus(postgresLogIds, 'no_recipients', 'Нет получателей после фильтрации');
return; return;
} }
@@ -347,27 +415,144 @@ async function sendTaskNotifications(type, taskId, taskTitle, taskDescription, a
formData.append('recipients', JSON.stringify(recipientIds)); formData.append('recipients', JSON.stringify(recipientIds));
formData.append('deliveryMethods', JSON.stringify(['email', 'telegram', 'vk'])); formData.append('deliveryMethods', JSON.stringify(['email', 'telegram', 'vk']));
const response = await fetch(process.env.NOTIFICATION_SERVICE_URL, { console.log(`🚀 Отправляем запрос на сервис уведомлений...`);
method: 'POST',
headers: {
'Authorization': `Basic ${authHeader}`
},
body: formData
});
if (!response.ok) { try {
throw new Error(`HTTP error! status: ${response.status}`); 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(); } catch (error) {
console.log(`✅ Уведомления отправлены для задачи ${taskId}:`, { console.error('❌ Общая ошибка при обработке уведомлений:', error);
type: type, }
recipients: recipientIds.length, }
authorExcluded: authorId
}); // Вспомогательные функции для работы с 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) { } 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')); 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, () => { app.listen(PORT, () => {
console.log(`CRM сервер запущен на порту ${PORT}`); console.log(`CRM сервер запущен на порту ${PORT}`);
console.log(`Откройте http://localhost:${PORT} в браузере`); console.log(`Откройте http://localhost:${PORT} в браузере`);