upravlenie-service

This commit is contained in:
2026-02-25 16:21:37 +05:00
parent 27fd3543ec
commit 0ece532121
2 changed files with 1059 additions and 680 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,23 +1,15 @@
// upravlenie-service.js // upravlenie-service.js
/**
* Модуль управления подключениями между сервисами
* Позволяет организовать цепочку сервисов (организатор -> исполнители)
* Каждый сервис имеет общий service_id (1-4062) для идентификации в цепочке
*/
const axios = require('axios'); const axios = require('axios');
const FormData = require('form-data'); const FormData = require('form-data');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const crypto = require('crypto'); const crypto = require('crypto');
// Константы
const SERVICE_ID_RANGE = { min: 1, max: 4062 }; const SERVICE_ID_RANGE = { min: 1, max: 4062 };
const SYNC_INTERVAL = 60000; // 1 минута const SYNC_INTERVAL = 60000;
const MAX_RETRY_COUNT = 3; const MAX_RETRY_COUNT = 3;
const RETRY_DELAY = 5000; // 5 секунд const RETRY_DELAY = 5000;
// Статусы задач для синхронизации
const TaskStatus = { const TaskStatus = {
PENDING: 'pending', PENDING: 'pending',
IN_PROGRESS: 'in_progress', IN_PROGRESS: 'in_progress',
@@ -28,24 +20,19 @@ const TaskStatus = {
CANCELLED: 'cancelled' CANCELLED: 'cancelled'
}; };
// Направление синхронизации
const SyncDirection = { const SyncDirection = {
INCOMING: 'incoming', // Получение задач от организатора INCOMING: 'incoming',
OUTGOING: 'outgoing' // Отправка статусов исполнителю OUTGOING: 'outgoing'
}; };
// Класс для работы с подключениями между сервисами
class UpravlenieService { class UpravlenieService {
constructor(db) { constructor(db) {
this.db = db; this.db = db;
this.syncIntervals = new Map(); // Интервалы синхронизации для каждого подключения this.syncIntervals = new Map();
this.syncInProgress = new Set(); // Подключения в процессе синхронизации this.syncInProgress = new Set();
this.init(); this.init();
} }
/**
* Инициализация сервиса
*/
async init() { async init() {
console.log('🔧 Инициализация сервиса Upravlenie...'); console.log('🔧 Инициализация сервиса Upravlenie...');
await this.createTable(); await this.createTable();
@@ -53,9 +40,6 @@ class UpravlenieService {
console.log('✅ Сервис Upravlenie инициализирован'); console.log('✅ Сервис Upravlenie инициализирован');
} }
/**
* Создание таблицы upravlenie
*/
async createTable() { async createTable() {
const createTableSQL = ` const createTableSQL = `
CREATE TABLE IF NOT EXISTS upravlenie ( CREATE TABLE IF NOT EXISTS upravlenie (
@@ -94,9 +78,6 @@ class UpravlenieService {
}); });
} }
/**
* Создание индексов для таблицы upravlenie
*/
async createIndexes() { async createIndexes() {
const indexes = [ const indexes = [
'CREATE INDEX IF NOT EXISTS idx_upravlenie_service_id ON upravlenie(service_id)', 'CREATE INDEX IF NOT EXISTS idx_upravlenie_service_id ON upravlenie(service_id)',
@@ -126,27 +107,21 @@ class UpravlenieService {
}); });
} }
/** async createConnection(data) {
* Создание нового подключения
*/
async createConnection(data) {
const { const {
service_id, service_name, service_type, login, password, service_id, service_name, service_type, login, password,
api_url, local_user_id, local_user_login, sync_direction = 'outgoing', api_url, local_user_id, local_user_login, sync_direction = 'outgoing',
sync_enabled = 1, sync_interval = 60 sync_enabled = 1, sync_interval = 60
} = data; } = data;
// Валидация service_id
if (service_id < SERVICE_ID_RANGE.min || service_id > SERVICE_ID_RANGE.max) { if (service_id < SERVICE_ID_RANGE.min || service_id > SERVICE_ID_RANGE.max) {
throw new Error(`service_id должен быть в диапазоне ${SERVICE_ID_RANGE.min}-${SERVICE_ID_RANGE.max}`); throw new Error(`service_id должен быть в диапазоне ${SERVICE_ID_RANGE.min}-${SERVICE_ID_RANGE.max}`);
} }
// Для организатора api_url может быть пустым
if (service_type === 'executor' && !api_url) { if (service_type === 'executor' && !api_url) {
throw new Error('Для исполнителя необходимо указать api_url организатора'); throw new Error('Для исполнителя необходимо указать api_url организатора');
} }
// Проверяем существование локального пользователя если указан
if (local_user_id) { if (local_user_id) {
const userExists = await this.checkLocalUserExists(local_user_id); const userExists = await this.checkLocalUserExists(local_user_id);
if (!userExists) { if (!userExists) {
@@ -154,7 +129,6 @@ async createConnection(data) {
} }
} }
// Проверяем уникальность service_id для активных подключений
const existing = await this.getConnectionByServiceId(service_id); const existing = await this.getConnectionByServiceId(service_id);
if (existing && existing.is_active) { if (existing && existing.is_active) {
throw new Error(`Подключение с service_id ${service_id} уже существует`); throw new Error(`Подключение с service_id ${service_id} уже существует`);
@@ -179,16 +153,20 @@ async createConnection(data) {
} else { } else {
const connectionId = this.lastID; const connectionId = this.lastID;
console.log(`✅ Создано подключение ${service_name} (ID: ${connectionId}, service_id: ${service_id})`); console.log(`✅ Создано подключение ${service_name} (ID: ${connectionId}, service_id: ${service_id})`);
resolve({ id: connectionId, ...data }); resolve({ id: connectionId, ...data });
} }
}); });
}); });
} }
async checkLocalUserExists(userId) {
return new Promise((resolve) => {
this.db.get('SELECT id FROM users WHERE id = ?', [userId], (err, row) => {
resolve(!!row);
});
});
}
/**
* Получение подключения по ID
*/
getConnection(id) { getConnection(id) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.db.get('SELECT * FROM upravlenie WHERE id = ?', [id], (err, row) => { this.db.get('SELECT * FROM upravlenie WHERE id = ?', [id], (err, row) => {
@@ -198,9 +176,6 @@ async createConnection(data) {
}); });
} }
/**
* Получение подключения по service_id
*/
getConnectionByServiceId(serviceId) { getConnectionByServiceId(serviceId) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.db.get( this.db.get(
@@ -214,9 +189,6 @@ async createConnection(data) {
}); });
} }
/**
* Получение всех подключений
*/
getAllConnections(filters = {}) { getAllConnections(filters = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let sql = 'SELECT * FROM upravlenie WHERE 1=1'; let sql = 'SELECT * FROM upravlenie WHERE 1=1';
@@ -246,9 +218,6 @@ async createConnection(data) {
}); });
} }
/**
* Обновление подключения
*/
async updateConnection(id, data) { async updateConnection(id, data) {
const fields = []; const fields = [];
const values = []; const values = [];
@@ -288,16 +257,10 @@ async createConnection(data) {
}); });
} }
/**
* Удаление подключения (мягкое удаление)
*/
async deleteConnection(id) { async deleteConnection(id) {
return this.updateConnection(id, { is_active: 0 }); return this.updateConnection(id, { is_active: 0 });
} }
/**
* Запуск всех задач синхронизации
*/
async startAllSyncJobs() { async startAllSyncJobs() {
const connections = await this.getAllConnections({ const connections = await this.getAllConnections({
is_active: true, is_active: true,
@@ -311,9 +274,6 @@ async createConnection(data) {
console.log(`✅ Запущено ${connections.length} задач синхронизации`); console.log(`✅ Запущено ${connections.length} задач синхронизации`);
} }
/**
* Запуск задачи синхронизации для конкретного подключения
*/
startSyncJob(connectionId, intervalMinutes) { startSyncJob(connectionId, intervalMinutes) {
this.stopSyncJob(connectionId); this.stopSyncJob(connectionId);
@@ -325,15 +285,11 @@ async createConnection(data) {
this.syncIntervals.set(connectionId, interval); this.syncIntervals.set(connectionId, interval);
console.log(`✅ Запущена синхронизация для подключения ${connectionId} (интервал: ${intervalMinutes} мин)`); console.log(`✅ Запущена синхронизация для подключения ${connectionId} (интервал: ${intervalMinutes} мин)`);
// Выполняем первую синхронизацию сразу
setTimeout(() => { setTimeout(() => {
this.syncConnection(connectionId); this.syncConnection(connectionId);
}, 1000); }, 1000);
} }
/**
* Остановка задачи синхронизации
*/
stopSyncJob(connectionId) { stopSyncJob(connectionId) {
if (this.syncIntervals.has(connectionId)) { if (this.syncIntervals.has(connectionId)) {
clearInterval(this.syncIntervals.get(connectionId)); clearInterval(this.syncIntervals.get(connectionId));
@@ -342,9 +298,6 @@ async createConnection(data) {
} }
} }
/**
* Перезапуск задачи синхронизации
*/
async restartSyncJob(connectionId) { async restartSyncJob(connectionId) {
this.stopSyncJob(connectionId); this.stopSyncJob(connectionId);
@@ -354,11 +307,7 @@ async createConnection(data) {
} }
} }
/**
* Синхронизация с подключенным сервисом
*/
async syncConnection(connectionId) { async syncConnection(connectionId) {
// Предотвращаем одновременную синхронизацию одного подключения
if (this.syncInProgress.has(connectionId)) { if (this.syncInProgress.has(connectionId)) {
console.log(`⚠️ Синхронизация подключения ${connectionId} уже выполняется`); console.log(`⚠️ Синхронизация подключения ${connectionId} уже выполняется`);
return; return;
@@ -374,14 +323,11 @@ async createConnection(data) {
console.log(`🔄 Синхронизация подключения ${connection.service_name} (${connection.service_id})...`); console.log(`🔄 Синхронизация подключения ${connection.service_name} (${connection.service_id})...`);
// Обновляем время последней синхронизации
await this.updateSyncStatus(connectionId, 'in_progress'); await this.updateSyncStatus(connectionId, 'in_progress');
if (connection.service_type === 'organizer') { if (connection.service_type === 'organizer') {
// Для организатора: получаем статусы от исполнителей
await this.syncFromExecutors(connection); await this.syncFromExecutors(connection);
} else { } else {
// Для исполнителя: отправляем статусы организатору
await this.syncToOrganizer(connection); await this.syncToOrganizer(connection);
} }
@@ -396,9 +342,6 @@ async createConnection(data) {
} }
} }
/**
* Обновление статуса синхронизации
*/
updateSyncStatus(connectionId, status, error = null) { updateSyncStatus(connectionId, status, error = null) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.db.run( this.db.run(
@@ -416,43 +359,52 @@ async createConnection(data) {
}); });
} }
/** async syncToOrganizer(connection) {
* Синхронизация от исполнителя к организатору
*/
async syncToOrganizer(connection) {
if (!connection.api_url) { if (!connection.api_url) {
throw new Error('Для исполнителя не указан api_url организатора'); throw new Error('Для исполнителя не указан api_url организатора');
} }
// Проверяем наличие локального пользователя console.log(`🔍 Исполнитель ${connection.service_name} пытается связаться с организатором по URL: ${connection.api_url}`);
if (!connection.local_user_id) { if (!connection.local_user_id) {
console.warn(`⚠️ Для подключения ${connection.service_name} не указан локальный пользователь. Задачи не будут создаваться.`); console.warn(`⚠️ Для подключения ${connection.service_name} не указан локальный пользователь. Задачи не будут создаваться.`);
return; return;
} }
// Получаем задачи, назначенные локальному пользователю try {
console.log(`📡 Проверка доступности организатора...`);
const organizerTasks = await this.fetchTasksFromOrganizer(connection);
console.log(`📥 Получено ${organizerTasks.length} задач от организатора`);
const localTasks = await this.getTasksForLocalUser(connection.local_user_id); const localTasks = await this.getTasksForLocalUser(connection.local_user_id);
// Получаем задачи от организатора if (organizerTasks.length > 0) {
const organizerTasks = await this.fetchTasksFromOrganizer(connection); await this.syncTasksWithOrganizer(connection, organizerTasks, localTasks);
if (organizerTasks.length === 0) {
console.log(`📭 Нет новых задач от организатора для ${connection.service_name}`);
return;
} }
// Обновляем локальные задачи if (localTasks.length > 0) {
await this.syncTasksWithOrganizer(connection, organizerTasks, localTasks);
// Отправляем обновленные статусы организатору
await this.sendTaskStatusesToOrganizer(connection, localTasks); await this.sendTaskStatusesToOrganizer(connection, localTasks);
} console.log(`📤 Отправлено ${localTasks.length} статусов организатору`);
}
/** } catch (error) {
* Синхронизация от организатора к исполнителям console.error(`❌ Ошибка связи с организатором:`, error.message);
*/ if (error.code === 'ECONNREFUSED') {
async syncFromExecutors(connection) { console.error(`🔴 Организатор не доступен по адресу ${connection.api_url}`);
// Получаем все активные подключения исполнителей с таким же service_id } else if (error.code === 'ETIMEDOUT') {
console.error(`⏱️ Таймаут при подключении к организатору`);
} else if (error.response) {
console.error(`📡 Ответ организатора:`, {
status: error.response.status,
data: error.response.data
});
}
throw error;
}
}
async syncFromExecutors(connection) {
const executors = await this.getAllConnections({ const executors = await this.getAllConnections({
service_type: 'executor', service_type: 'executor',
is_active: true, is_active: true,
@@ -466,17 +418,14 @@ async syncFromExecutors(connection) {
for (const executor of executors) { for (const executor of executors) {
try { try {
// Проверяем наличие локального пользователя у исполнителя
if (!executor.local_user_id) { if (!executor.local_user_id) {
console.warn(`⚠️ Для исполнителя ${executor.service_name} не указан локальный пользователь. Пропускаем...`); console.warn(`⚠️ Для исполнителя ${executor.service_name} не указан локальный пользователь. Пропускаем...`);
continue; continue;
} }
// Получаем статусы задач от исполнителя
const taskStatuses = await this.fetchTaskStatusesFromExecutor(executor); const taskStatuses = await this.fetchTaskStatusesFromExecutor(executor);
if (taskStatuses.length > 0) { if (taskStatuses.length > 0) {
// Обновляем статусы в локальной БД
await this.updateTaskStatusesFromExecutor(executor, taskStatuses); await this.updateTaskStatusesFromExecutor(executor, taskStatuses);
console.log(`✅ Получено ${taskStatuses.length} статусов от исполнителя ${executor.service_name}`); console.log(`✅ Получено ${taskStatuses.length} статусов от исполнителя ${executor.service_name}`);
} }
@@ -485,20 +434,8 @@ async syncFromExecutors(connection) {
console.error(`❌ Ошибка получения статусов от исполнителя ${executor.service_name}:`, error.message); console.error(`❌ Ошибка получения статусов от исполнителя ${executor.service_name}:`, error.message);
} }
} }
} }
/**
* Проверка существования локального пользователя
*/
async checkLocalUserExists(userId) {
return new Promise((resolve) => {
this.db.get('SELECT id FROM users WHERE id = ?', [userId], (err, row) => {
resolve(!!row);
});
});
}
/**
* Получение задач для локального пользователя
*/
getTasksForLocalUser(userId) { getTasksForLocalUser(userId) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!userId) { if (!userId) {
@@ -518,9 +455,6 @@ async checkLocalUserExists(userId) {
}); });
} }
/**
* Получение задач от организатора через API
*/
async fetchTasksFromOrganizer(connection) { async fetchTasksFromOrganizer(connection) {
try { try {
const response = await axios.get(`${connection.api_url}/api/external/tasks`, { const response = await axios.get(`${connection.api_url}/api/external/tasks`, {
@@ -542,9 +476,6 @@ async checkLocalUserExists(userId) {
} }
} }
/**
* Получение статусов задач от исполнителя
*/
async fetchTaskStatusesFromExecutor(executor) { async fetchTaskStatusesFromExecutor(executor) {
try { try {
const response = await axios.get(`${executor.api_url}/api/external/task-statuses`, { const response = await axios.get(`${executor.api_url}/api/external/task-statuses`, {
@@ -565,9 +496,6 @@ async checkLocalUserExists(userId) {
} }
} }
/**
* Отправка статусов задач организатору
*/
async sendTaskStatusesToOrganizer(connection, localTasks) { async sendTaskStatusesToOrganizer(connection, localTasks) {
const statuses = localTasks.map(task => ({ const statuses = localTasks.map(task => ({
task_id: task.id, task_id: task.id,
@@ -596,37 +524,25 @@ async checkLocalUserExists(userId) {
} }
} }
/**
* Синхронизация задач с организатором
*/
async syncTasksWithOrganizer(connection, organizerTasks, localTasks) { async syncTasksWithOrganizer(connection, organizerTasks, localTasks) {
for (const organizerTask of organizerTasks) { for (const organizerTask of organizerTasks) {
const localTask = localTasks.find(t => t.external_task_id === organizerTask.id); const localTask = localTasks.find(t => t.external_task_id === organizerTask.id);
if (!localTask) { if (!localTask) {
// Новая задача от организатора - создаем локально
await this.createTaskFromOrganizer(connection, organizerTask); await this.createTaskFromOrganizer(connection, organizerTask);
} else { } else {
// Существующая задача - обновляем если нужно
await this.updateTaskFromOrganizer(connection, organizerTask, localTask); await this.updateTaskFromOrganizer(connection, organizerTask, localTask);
} }
} }
} }
// upravlenie-service.js (исправленная версия метода createTaskFromOrganizer) async createTaskFromOrganizer(connection, taskData) {
/**
* Создание задачи из данных организатора
*/
async createTaskFromOrganizer(connection, taskData) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// Проверяем, что local_user_id не NULL
if (!connection.local_user_id) { if (!connection.local_user_id) {
reject(new Error('local_user_id не может быть пустым. Укажите локального пользователя для создания задач.')); reject(new Error('local_user_id не может быть пустым. Укажите локального пользователя для создания задач.'));
return; return;
} }
// Сначала проверяем структуру таблицы tasks
this.db.all("PRAGMA table_info(tasks)", (err, columns) => { this.db.all("PRAGMA table_info(tasks)", (err, columns) => {
if (err) { if (err) {
reject(err); reject(err);
@@ -635,27 +551,24 @@ async createTaskFromOrganizer(connection, taskData) {
const columnNames = columns.map(c => c.name); const columnNames = columns.map(c => c.name);
// Формируем SQL запрос динамически
let fields = ['title', 'description', 'status', 'created_by', 'start_date', 'due_date', 'task_type']; let fields = ['title', 'description', 'status', 'created_by', 'start_date', 'due_date', 'task_type'];
let placeholders = ['?', '?', '?', '?', '?', '?', '?']; let placeholders = ['?', '?', '?', '?', '?', '?', '?'];
let values = [ let values = [
taskData.title, taskData.title,
taskData.description || '', taskData.description || '',
'active', 'active',
connection.local_user_id, // теперь точно не NULL connection.local_user_id,
taskData.start_date || new Date().toISOString(), taskData.start_date || new Date().toISOString(),
taskData.due_date || null, taskData.due_date || null,
'external' 'external'
]; ];
// Добавляем external_task_id если колонка существует
if (columnNames.includes('external_task_id')) { if (columnNames.includes('external_task_id')) {
fields.push('external_task_id'); fields.push('external_task_id');
placeholders.push('?'); placeholders.push('?');
values.push(taskData.id); values.push(taskData.id);
} }
// Добавляем external_service_id если колонка существует
if (columnNames.includes('external_service_id')) { if (columnNames.includes('external_service_id')) {
fields.push('external_service_id'); fields.push('external_service_id');
placeholders.push('?'); placeholders.push('?');
@@ -664,23 +577,14 @@ async createTaskFromOrganizer(connection, taskData) {
const sql = `INSERT INTO tasks (${fields.join(', ')}) VALUES (${placeholders.join(', ')})`; const sql = `INSERT INTO tasks (${fields.join(', ')}) VALUES (${placeholders.join(', ')})`;
console.log(`📝 Создание задачи от организатора:`, {
sql,
values,
local_user_id: connection.local_user_id,
service_id: connection.service_id
});
this.db.run(sql, values, function(err) { this.db.run(sql, values, function(err) {
if (err) { if (err) {
console.error('❌ Ошибка создания задачи:', err);
reject(err); reject(err);
return; return;
} }
const newTaskId = this.lastID; const newTaskId = this.lastID;
// Назначаем задачу локальному пользователю
this.db.run( this.db.run(
`INSERT INTO task_assignments (task_id, user_id, status, start_date, due_date) `INSERT INTO task_assignments (task_id, user_id, status, start_date, due_date)
VALUES (?, ?, ?, ?, ?)`, VALUES (?, ?, ?, ?, ?)`,
@@ -693,7 +597,6 @@ async createTaskFromOrganizer(connection, taskData) {
], ],
(err) => { (err) => {
if (err) { if (err) {
console.error('❌ Ошибка назначения задачи:', err);
reject(err); reject(err);
return; return;
} }
@@ -705,10 +608,8 @@ async createTaskFromOrganizer(connection, taskData) {
}); });
}); });
}); });
} }
/**
* Обновление задачи из данных организатора
*/
async updateTaskFromOrganizer(connection, organizerTask, localTask) { async updateTaskFromOrganizer(connection, organizerTask, localTask) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.db.run( this.db.run(
@@ -727,9 +628,6 @@ async createTaskFromOrganizer(connection, taskData) {
}); });
} }
/**
* Обновление статусов задач от исполнителя
*/
async updateTaskStatusesFromExecutor(executor, taskStatuses) { async updateTaskStatusesFromExecutor(executor, taskStatuses) {
for (const status of taskStatuses) { for (const status of taskStatuses) {
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
@@ -754,16 +652,12 @@ async createTaskFromOrganizer(connection, taskData) {
); );
}); });
// Если статус "completed" - проверяем и закрываем задачу если все выполнили
if (status.status === 'completed') { if (status.status === 'completed') {
await this.checkAndCloseTaskIfAllCompleted(status.task_id); await this.checkAndCloseTaskIfAllCompleted(status.task_id);
} }
} }
} }
/**
* Проверка и закрытие задачи если все исполнители выполнили
*/
async checkAndCloseTaskIfAllCompleted(taskId) { async checkAndCloseTaskIfAllCompleted(taskId) {
return new Promise((resolve) => { return new Promise((resolve) => {
this.db.all( this.db.all(
@@ -795,9 +689,6 @@ async createTaskFromOrganizer(connection, taskData) {
}); });
} }
/**
* Загрузка файла в удаленный сервис
*/
async uploadFileToRemote(connection, taskId, fileId) { async uploadFileToRemote(connection, taskId, fileId) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.db.get( this.db.get(
@@ -850,9 +741,6 @@ async createTaskFromOrganizer(connection, taskData) {
}); });
} }
/**
* Получение файла из удаленного сервиса
*/
async downloadFileFromRemote(connection, remoteFileId, localTaskId) { async downloadFileFromRemote(connection, remoteFileId, localTaskId) {
try { try {
const response = await axios.get( const response = await axios.get(
@@ -868,7 +756,6 @@ async createTaskFromOrganizer(connection, taskData) {
} }
); );
// Сохраняем файл локально
const { createUserTaskFolder } = require('./database'); const { createUserTaskFolder } = require('./database');
const userFolder = createUserTaskFolder(localTaskId, connection.local_user_login || 'external'); const userFolder = createUserTaskFolder(localTaskId, connection.local_user_login || 'external');
@@ -883,7 +770,6 @@ async createTaskFromOrganizer(connection, taskData) {
writer.on('error', reject); writer.on('error', reject);
}); });
// Сохраняем запись в БД
const fileSize = fs.statSync(filePath).size; const fileSize = fs.statSync(filePath).size;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -914,9 +800,6 @@ async createTaskFromOrganizer(connection, taskData) {
} }
} }
/**
* Генерация токена для внешней аутентификации
*/
generateAuthToken(serviceId, login) { generateAuthToken(serviceId, login) {
const secret = process.env.EXTERNAL_API_SECRET || 'default_secret_change_me'; const secret = process.env.EXTERNAL_API_SECRET || 'default_secret_change_me';
const timestamp = Math.floor(Date.now() / 1000); const timestamp = Math.floor(Date.now() / 1000);
@@ -925,15 +808,11 @@ async createTaskFromOrganizer(connection, taskData) {
return `${timestamp}:${hash}`; return `${timestamp}:${hash}`;
} }
/**
* Проверка токена внешней аутентификации
*/
verifyAuthToken(token, serviceId, login) { verifyAuthToken(token, serviceId, login) {
try { try {
const secret = process.env.EXTERNAL_API_SECRET || 'default_secret_change_me'; const secret = process.env.EXTERNAL_API_SECRET || 'default_secret_change_me';
const [timestamp, hash] = token.split(':'); const [timestamp, hash] = token.split(':');
// Проверяем, что токен не старше 5 минут
const now = Math.floor(Date.now() / 1000); const now = Math.floor(Date.now() / 1000);
if (now - parseInt(timestamp) > 300) { if (now - parseInt(timestamp) > 300) {
return false; return false;
@@ -951,17 +830,73 @@ async createTaskFromOrganizer(connection, taskData) {
} }
} }
/**
* Ручная синхронизация подключения
*/
async manualSync(connectionId) { async manualSync(connectionId) {
console.log(`🔄 Ручная синхронизация подключения ${connectionId}`); console.log(`🔄 Ручная синхронизация подключения ${connectionId}`);
await this.syncConnection(connectionId); await this.syncConnection(connectionId);
} }
/** async checkConnection(connectionId) {
* Получение статистики синхронизации const connection = await this.getConnection(connectionId);
*/ if (!connection) {
return { success: false, error: 'Подключение не найдено' };
}
const result = {
id: connection.id,
service_id: connection.service_id,
service_name: connection.service_name,
service_type: connection.service_type,
local_user_id: connection.local_user_id,
api_url: connection.api_url,
status: 'unknown'
};
if (connection.service_type === 'executor') {
if (!connection.api_url) {
result.status = 'error';
result.error = 'Не указан API URL организатора';
return result;
}
try {
const response = await axios.get(`${connection.api_url}/api/health`, {
timeout: 5000,
auth: {
username: connection.login,
password: connection.password
}
});
result.status = 'connected';
result.organizer_status = response.data;
} catch (error) {
result.status = 'disconnected';
result.error = error.message;
if (error.code === 'ECONNREFUSED') {
result.error_details = 'Организатор не доступен по указанному адресу';
} else if (error.response?.status === 401) {
result.error_details = 'Ошибка аутентификации - проверьте логин/пароль';
} else if (error.response?.status === 404) {
result.error_details = 'API организатора не найдено - проверьте URL';
} else {
result.error_details = error.message;
}
}
} else {
const executors = await this.getAllConnections({
service_type: 'executor',
is_active: true
});
result.executors_count = executors.length;
result.status = executors.length > 0 ? 'has_executors' : 'no_executors';
}
return result;
}
async getSyncStats() { async getSyncStats() {
const connections = await this.getAllConnections({ is_active: true }); const connections = await this.getAllConnections({ is_active: true });
@@ -1001,11 +936,9 @@ async createTaskFromOrganizer(connection, taskData) {
} }
} }
// Функция для настройки API endpoints
function setupUpravlenieEndpoints(app, db) { function setupUpravlenieEndpoints(app, db) {
const upravlenieService = new UpravlenieService(db); const upravlenieService = new UpravlenieService(db);
// Middleware для проверки аутентификации
const requireAuth = (req, res, next) => { const requireAuth = (req, res, next) => {
if (!req.session || !req.session.user) { if (!req.session || !req.session.user) {
return res.status(401).json({ error: 'Требуется аутентификация' }); return res.status(401).json({ error: 'Требуется аутентификация' });
@@ -1013,7 +946,6 @@ function setupUpravlenieEndpoints(app, db) {
next(); next();
}; };
// Middleware для проверки прав администратора
const requireAdmin = (req, res, next) => { const requireAdmin = (req, res, next) => {
if (!req.session || !req.session.user) { if (!req.session || !req.session.user) {
return res.status(401).json({ error: 'Требуется аутентификация' }); return res.status(401).json({ error: 'Требуется аутентификация' });
@@ -1024,12 +956,6 @@ function setupUpravlenieEndpoints(app, db) {
next(); next();
}; };
// ==================== ВНУТРЕННИЕ API (для администрирования) ====================
/**
* GET /api/upravlenie/connections
* Получение всех подключений
*/
app.get('/api/upravlenie/connections', requireAdmin, async (req, res) => { app.get('/api/upravlenie/connections', requireAdmin, async (req, res) => {
try { try {
const filters = { const filters = {
@@ -1046,10 +972,6 @@ function setupUpravlenieEndpoints(app, db) {
} }
}); });
/**
* GET /api/upravlenie/connections/:id
* Получение подключения по ID
*/
app.get('/api/upravlenie/connections/:id', requireAdmin, async (req, res) => { app.get('/api/upravlenie/connections/:id', requireAdmin, async (req, res) => {
try { try {
const connection = await upravlenieService.getConnection(req.params.id); const connection = await upravlenieService.getConnection(req.params.id);
@@ -1062,10 +984,6 @@ function setupUpravlenieEndpoints(app, db) {
} }
}); });
/**
* POST /api/upravlenie/connections
* Создание нового подключения
*/
app.post('/api/upravlenie/connections', requireAdmin, async (req, res) => { app.post('/api/upravlenie/connections', requireAdmin, async (req, res) => {
try { try {
const connection = await upravlenieService.createConnection(req.body); const connection = await upravlenieService.createConnection(req.body);
@@ -1080,10 +998,6 @@ function setupUpravlenieEndpoints(app, db) {
} }
}); });
/**
* PUT /api/upravlenie/connections/:id
* Обновление подключения
*/
app.put('/api/upravlenie/connections/:id', requireAdmin, async (req, res) => { app.put('/api/upravlenie/connections/:id', requireAdmin, async (req, res) => {
try { try {
const result = await upravlenieService.updateConnection(req.params.id, req.body); const result = await upravlenieService.updateConnection(req.params.id, req.body);
@@ -1097,10 +1011,6 @@ function setupUpravlenieEndpoints(app, db) {
} }
}); });
/**
* DELETE /api/upravlenie/connections/:id
* Удаление подключения
*/
app.delete('/api/upravlenie/connections/:id', requireAdmin, async (req, res) => { app.delete('/api/upravlenie/connections/:id', requireAdmin, async (req, res) => {
try { try {
await upravlenieService.deleteConnection(req.params.id); await upravlenieService.deleteConnection(req.params.id);
@@ -1110,10 +1020,6 @@ function setupUpravlenieEndpoints(app, db) {
} }
}); });
/**
* POST /api/upravlenie/connections/:id/sync
* Ручная синхронизация подключения
*/
app.post('/api/upravlenie/connections/:id/sync', requireAdmin, async (req, res) => { app.post('/api/upravlenie/connections/:id/sync', requireAdmin, async (req, res) => {
try { try {
await upravlenieService.manualSync(req.params.id); await upravlenieService.manualSync(req.params.id);
@@ -1123,10 +1029,15 @@ function setupUpravlenieEndpoints(app, db) {
} }
}); });
/** app.get('/api/upravlenie/check/:id', requireAdmin, async (req, res) => {
* GET /api/upravlenie/stats try {
* Статистика синхронизации const result = await upravlenieService.checkConnection(req.params.id);
*/ res.json(result);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.get('/api/upravlenie/stats', requireAdmin, async (req, res) => { app.get('/api/upravlenie/stats', requireAdmin, async (req, res) => {
try { try {
const stats = await upravlenieService.getSyncStats(); const stats = await upravlenieService.getSyncStats();
@@ -1136,11 +1047,6 @@ function setupUpravlenieEndpoints(app, db) {
} }
}); });
// ==================== ВНЕШНИЕ API (для взаимодействия между сервисами) ====================
/**
* Middleware для аутентификации внешних запросов
*/
const authenticateExternal = async (req, res, next) => { const authenticateExternal = async (req, res, next) => {
const authHeader = req.headers.authorization; const authHeader = req.headers.authorization;
@@ -1153,7 +1059,6 @@ function setupUpravlenieEndpoints(app, db) {
const [login, password] = credentials.split(':'); const [login, password] = credentials.split(':');
try { try {
// Ищем подключение по логину и паролю
const connection = await new Promise((resolve, reject) => { const connection = await new Promise((resolve, reject) => {
db.get( db.get(
'SELECT * FROM upravlenie WHERE login = ? AND password = ? AND is_active = 1', 'SELECT * FROM upravlenie WHERE login = ? AND password = ? AND is_active = 1',
@@ -1177,22 +1082,15 @@ function setupUpravlenieEndpoints(app, db) {
} }
}; };
/**
* GET /api/external/tasks
* Получение задач для внешнего сервиса
* (используется исполнителем для получения задач от организатора)
*/
app.get('/api/external/tasks', authenticateExternal, async (req, res) => { app.get('/api/external/tasks', authenticateExternal, async (req, res) => {
try { try {
const { service_id, user_login } = req.query; const { service_id, user_login } = req.query;
const connection = req.externalConnection; const connection = req.externalConnection;
// Проверяем соответствие service_id
if (parseInt(service_id) !== connection.service_id) { if (parseInt(service_id) !== connection.service_id) {
return res.status(403).json({ error: 'Неверный service_id' }); return res.status(403).json({ error: 'Неверный service_id' });
} }
// Если указан user_login, ищем задачи для этого пользователя
let userId = null; let userId = null;
if (user_login) { if (user_login) {
const user = await new Promise((resolve) => { const user = await new Promise((resolve) => {
@@ -1203,10 +1101,8 @@ function setupUpravlenieEndpoints(app, db) {
userId = user?.id; userId = user?.id;
} }
// Получаем задачи
let tasks = []; let tasks = [];
if (userId) { if (userId) {
// Задачи для конкретного пользователя
tasks = await new Promise((resolve) => { tasks = await new Promise((resolve) => {
db.all(` db.all(`
SELECT t.*, ta.status as assignment_status SELECT t.*, ta.status as assignment_status
@@ -1218,7 +1114,6 @@ function setupUpravlenieEndpoints(app, db) {
}); });
}); });
} else { } else {
// Все активные задачи
tasks = await new Promise((resolve) => { tasks = await new Promise((resolve) => {
db.all(` db.all(`
SELECT t.*, GROUP_CONCAT(ta.user_id) as assigned_users SELECT t.*, GROUP_CONCAT(ta.user_id) as assigned_users
@@ -1251,10 +1146,6 @@ function setupUpravlenieEndpoints(app, db) {
} }
}); });
/**
* POST /api/external/task-statuses
* Получение статусов задач от исполнителя
*/
app.post('/api/external/task-statuses', authenticateExternal, async (req, res) => { app.post('/api/external/task-statuses', authenticateExternal, async (req, res) => {
try { try {
const { service_id, statuses } = req.body; const { service_id, statuses } = req.body;
@@ -1268,7 +1159,6 @@ function setupUpravlenieEndpoints(app, db) {
return res.status(400).json({ error: 'statuses должен быть массивом' }); return res.status(400).json({ error: 'statuses должен быть массивом' });
} }
// Обновляем статусы в локальной БД
for (const status of statuses) { for (const status of statuses) {
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
db.run( db.run(
@@ -1283,7 +1173,6 @@ function setupUpravlenieEndpoints(app, db) {
); );
}); });
// Если статус "completed" - закрываем задачу если все исполнители выполнили
if (status.status === 'completed') { if (status.status === 'completed') {
await checkAndCloseTaskIfAllCompleted(db, status.task_id); await checkAndCloseTaskIfAllCompleted(db, status.task_id);
} }
@@ -1301,10 +1190,6 @@ function setupUpravlenieEndpoints(app, db) {
} }
}); });
/**
* GET /api/external/task-statuses
* Получение статусов задач для организатора
*/
app.get('/api/external/task-statuses', authenticateExternal, async (req, res) => { app.get('/api/external/task-statuses', authenticateExternal, async (req, res) => {
try { try {
const { service_id } = req.query; const { service_id } = req.query;
@@ -1314,7 +1199,6 @@ function setupUpravlenieEndpoints(app, db) {
return res.status(403).json({ error: 'Неверный service_id' }); return res.status(403).json({ error: 'Неверный service_id' });
} }
// Получаем задачи, созданные для этого внешнего сервиса
const statuses = await new Promise((resolve) => { const statuses = await new Promise((resolve) => {
db.all(` db.all(`
SELECT SELECT
@@ -1342,10 +1226,6 @@ function setupUpravlenieEndpoints(app, db) {
} }
}); });
/**
* POST /api/external/upload-file
* Загрузка файла от исполнителя
*/
app.post('/api/external/upload-file', authenticateExternal, async (req, res) => { app.post('/api/external/upload-file', authenticateExternal, async (req, res) => {
if (!req.files || !req.files.file) { if (!req.files || !req.files.file) {
return res.status(400).json({ error: 'Файл не загружен' }); return res.status(400).json({ error: 'Файл не загружен' });
@@ -1360,17 +1240,14 @@ function setupUpravlenieEndpoints(app, db) {
return res.status(403).json({ error: 'Неверный service_id' }); return res.status(403).json({ error: 'Неверный service_id' });
} }
// Создаем папку для задачи
const { createUserTaskFolder } = require('./database'); const { createUserTaskFolder } = require('./database');
const userFolder = createUserTaskFolder(task_id, connection.local_user_login || 'external'); const userFolder = createUserTaskFolder(task_id, connection.local_user_login || 'external');
// Сохраняем файл
const fileName = `${Date.now()}_${file.name}`; const fileName = `${Date.now()}_${file.name}`;
const filePath = path.join(userFolder, fileName); const filePath = path.join(userFolder, fileName);
await file.mv(filePath); await file.mv(filePath);
// Сохраняем запись в БД
const result = await new Promise((resolve, reject) => { const result = await new Promise((resolve, reject) => {
db.run( db.run(
`INSERT INTO task_files (task_id, user_id, filename, original_name, file_path, file_size) `INSERT INTO task_files (task_id, user_id, filename, original_name, file_path, file_size)
@@ -1402,10 +1279,6 @@ function setupUpravlenieEndpoints(app, db) {
} }
}); });
/**
* GET /api/external/download-file/:fileId
* Скачивание файла для исполнителя
*/
app.get('/api/external/download-file/:fileId', authenticateExternal, async (req, res) => { app.get('/api/external/download-file/:fileId', authenticateExternal, async (req, res) => {
try { try {
const { fileId } = req.params; const { fileId } = req.params;
@@ -1416,7 +1289,6 @@ function setupUpravlenieEndpoints(app, db) {
return res.status(403).json({ error: 'Неверный service_id' }); return res.status(403).json({ error: 'Неверный service_id' });
} }
// Получаем информацию о файле
const file = await new Promise((resolve, reject) => { const file = await new Promise((resolve, reject) => {
db.get( db.get(
'SELECT * FROM task_files WHERE id = ?', 'SELECT * FROM task_files WHERE id = ?',
@@ -1436,7 +1308,6 @@ function setupUpravlenieEndpoints(app, db) {
return res.status(404).json({ error: 'Файл не существует на диске' }); return res.status(404).json({ error: 'Файл не существует на диске' });
} }
// Отправляем файл
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(file.original_name)}"`); res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(file.original_name)}"`);
res.setHeader('X-File-Name', encodeURIComponent(file.original_name)); res.setHeader('X-File-Name', encodeURIComponent(file.original_name));
res.setHeader('Content-Type', 'application/octet-stream'); res.setHeader('Content-Type', 'application/octet-stream');
@@ -1449,7 +1320,6 @@ function setupUpravlenieEndpoints(app, db) {
} }
}); });
// Вспомогательная функция для проверки и закрытия задачи
async function checkAndCloseTaskIfAllCompleted(db, taskId) { async function checkAndCloseTaskIfAllCompleted(db, taskId) {
return new Promise((resolve) => { return new Promise((resolve) => {
db.all( db.all(