1492 lines
57 KiB
JavaScript
1492 lines
57 KiB
JavaScript
// upravlenie-service.js
|
||
/**
|
||
* Модуль управления подключениями между сервисами
|
||
* Позволяет организовать цепочку сервисов (организатор -> исполнители)
|
||
* Каждый сервис имеет общий service_id (1-4062) для идентификации в цепочке
|
||
*/
|
||
|
||
const axios = require('axios');
|
||
const FormData = require('form-data');
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const crypto = require('crypto');
|
||
|
||
// Константы
|
||
const SERVICE_ID_RANGE = { min: 1, max: 4062 };
|
||
const SYNC_INTERVAL = 60000; // 1 минута
|
||
const MAX_RETRY_COUNT = 3;
|
||
const RETRY_DELAY = 5000; // 5 секунд
|
||
|
||
// Статусы задач для синхронизации
|
||
const TaskStatus = {
|
||
PENDING: 'pending',
|
||
IN_PROGRESS: 'in_progress',
|
||
ASSIGNED: 'assigned',
|
||
REWORK: 'rework',
|
||
COMPLETED: 'completed',
|
||
OVERDUE: 'overdue',
|
||
CANCELLED: 'cancelled'
|
||
};
|
||
|
||
// Направление синхронизации
|
||
const SyncDirection = {
|
||
INCOMING: 'incoming', // Получение задач от организатора
|
||
OUTGOING: 'outgoing' // Отправка статусов исполнителю
|
||
};
|
||
|
||
// Класс для работы с подключениями между сервисами
|
||
class UpravlenieService {
|
||
constructor(db) {
|
||
this.db = db;
|
||
this.syncIntervals = new Map(); // Интервалы синхронизации для каждого подключения
|
||
this.syncInProgress = new Set(); // Подключения в процессе синхронизации
|
||
this.init();
|
||
}
|
||
|
||
/**
|
||
* Инициализация сервиса
|
||
*/
|
||
async init() {
|
||
console.log('🔧 Инициализация сервиса Upravlenie...');
|
||
await this.createTable();
|
||
await this.startAllSyncJobs();
|
||
console.log('✅ Сервис Upravlenie инициализирован');
|
||
}
|
||
|
||
/**
|
||
* Создание таблицы upravlenie
|
||
*/
|
||
async createTable() {
|
||
const createTableSQL = `
|
||
CREATE TABLE IF NOT EXISTS upravlenie (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
service_id INTEGER NOT NULL CHECK(service_id >= 1 AND service_id <= 4062),
|
||
service_name VARCHAR(255) NOT NULL,
|
||
service_type VARCHAR(50) NOT NULL CHECK(service_type IN ('organizer', 'executor')),
|
||
login VARCHAR(255) NOT NULL,
|
||
password VARCHAR(255) NOT NULL,
|
||
api_url VARCHAR(500),
|
||
local_user_id INTEGER,
|
||
local_user_login VARCHAR(255),
|
||
sync_direction VARCHAR(50) DEFAULT 'outgoing' CHECK(sync_direction IN ('incoming', 'outgoing', 'both')),
|
||
sync_enabled BOOLEAN DEFAULT 1,
|
||
sync_interval INTEGER DEFAULT 60,
|
||
last_sync_at TIMESTAMP,
|
||
last_sync_status VARCHAR(50),
|
||
last_sync_error TEXT,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
is_active BOOLEAN DEFAULT 1,
|
||
FOREIGN KEY (local_user_id) REFERENCES users(id)
|
||
)
|
||
`;
|
||
|
||
return new Promise((resolve, reject) => {
|
||
this.db.run(createTableSQL, (err) => {
|
||
if (err) {
|
||
console.error('❌ Ошибка создания таблицы upravlenie:', err);
|
||
reject(err);
|
||
} else {
|
||
console.log('✅ Таблица upravlenie создана/проверена');
|
||
this.createIndexes().then(resolve).catch(reject);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Создание индексов для таблицы upravlenie
|
||
*/
|
||
async createIndexes() {
|
||
const indexes = [
|
||
'CREATE INDEX IF NOT EXISTS idx_upravlenie_service_id ON upravlenie(service_id)',
|
||
'CREATE INDEX IF NOT EXISTS idx_upravlenie_service_type ON upravlenie(service_type)',
|
||
'CREATE INDEX IF NOT EXISTS idx_upravlenie_local_user_id ON upravlenie(local_user_id)',
|
||
'CREATE INDEX IF NOT EXISTS idx_upravlenie_sync_enabled ON upravlenie(sync_enabled)',
|
||
'CREATE INDEX IF NOT EXISTS idx_upravlenie_last_sync ON upravlenie(last_sync_at)'
|
||
];
|
||
|
||
return new Promise((resolve, reject) => {
|
||
this.db.serialize(() => {
|
||
let error = null;
|
||
indexes.forEach(indexSQL => {
|
||
this.db.run(indexSQL, (err) => {
|
||
if (err) error = err;
|
||
});
|
||
});
|
||
|
||
if (error) {
|
||
console.error('❌ Ошибка создания индексов:', error);
|
||
reject(error);
|
||
} else {
|
||
console.log('✅ Индексы для upravlenie созданы');
|
||
resolve();
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Создание нового подключения
|
||
*/
|
||
async createConnection(data) {
|
||
const {
|
||
service_id, service_name, service_type, login, password,
|
||
api_url, local_user_id, local_user_login, sync_direction = 'outgoing',
|
||
sync_enabled = 1, sync_interval = 60
|
||
} = data;
|
||
|
||
// Валидация service_id
|
||
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}`);
|
||
}
|
||
|
||
// Для организатора api_url может быть пустым
|
||
if (service_type === 'executor' && !api_url) {
|
||
throw new Error('Для исполнителя необходимо указать api_url организатора');
|
||
}
|
||
|
||
// Проверяем существование локального пользователя если указан
|
||
if (local_user_id) {
|
||
const userExists = await this.checkLocalUserExists(local_user_id);
|
||
if (!userExists) {
|
||
throw new Error(`Пользователь с ID ${local_user_id} не существует`);
|
||
}
|
||
}
|
||
|
||
// Проверяем уникальность service_id для активных подключений
|
||
const existing = await this.getConnectionByServiceId(service_id);
|
||
if (existing && existing.is_active) {
|
||
throw new Error(`Подключение с service_id ${service_id} уже существует`);
|
||
}
|
||
|
||
const sql = `
|
||
INSERT INTO upravlenie (
|
||
service_id, service_name, service_type, login, password,
|
||
api_url, local_user_id, local_user_login, sync_direction,
|
||
sync_enabled, sync_interval
|
||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||
`;
|
||
|
||
return new Promise((resolve, reject) => {
|
||
this.db.run(sql, [
|
||
service_id, service_name, service_type, login, password,
|
||
api_url, local_user_id || null, local_user_login || null,
|
||
sync_direction, sync_enabled, sync_interval
|
||
], function(err) {
|
||
if (err) {
|
||
reject(err);
|
||
} else {
|
||
const connectionId = this.lastID;
|
||
console.log(`✅ Создано подключение ${service_name} (ID: ${connectionId}, service_id: ${service_id})`);
|
||
|
||
resolve({ id: connectionId, ...data });
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Получение подключения по ID
|
||
*/
|
||
getConnection(id) {
|
||
return new Promise((resolve, reject) => {
|
||
this.db.get('SELECT * FROM upravlenie WHERE id = ?', [id], (err, row) => {
|
||
if (err) reject(err);
|
||
else resolve(row);
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Получение подключения по service_id
|
||
*/
|
||
getConnectionByServiceId(serviceId) {
|
||
return new Promise((resolve, reject) => {
|
||
this.db.get(
|
||
'SELECT * FROM upravlenie WHERE service_id = ? AND is_active = 1',
|
||
[serviceId],
|
||
(err, row) => {
|
||
if (err) reject(err);
|
||
else resolve(row);
|
||
}
|
||
);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Получение всех подключений
|
||
*/
|
||
getAllConnections(filters = {}) {
|
||
return new Promise((resolve, reject) => {
|
||
let sql = 'SELECT * FROM upravlenie WHERE 1=1';
|
||
const params = [];
|
||
|
||
if (filters.service_type) {
|
||
sql += ' AND service_type = ?';
|
||
params.push(filters.service_type);
|
||
}
|
||
|
||
if (filters.is_active !== undefined) {
|
||
sql += ' AND is_active = ?';
|
||
params.push(filters.is_active ? 1 : 0);
|
||
}
|
||
|
||
if (filters.sync_enabled !== undefined) {
|
||
sql += ' AND sync_enabled = ?';
|
||
params.push(filters.sync_enabled ? 1 : 0);
|
||
}
|
||
|
||
sql += ' ORDER BY service_name';
|
||
|
||
this.db.all(sql, params, (err, rows) => {
|
||
if (err) reject(err);
|
||
else resolve(rows);
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Обновление подключения
|
||
*/
|
||
async updateConnection(id, data) {
|
||
const fields = [];
|
||
const values = [];
|
||
|
||
const allowedFields = [
|
||
'service_name', 'service_type', 'login', 'password',
|
||
'api_url', 'local_user_id', 'local_user_login',
|
||
'sync_direction', 'sync_enabled', 'sync_interval',
|
||
'is_active'
|
||
];
|
||
|
||
allowedFields.forEach(field => {
|
||
if (data[field] !== undefined) {
|
||
fields.push(`${field} = ?`);
|
||
values.push(data[field]);
|
||
}
|
||
});
|
||
|
||
if (fields.length === 0) {
|
||
throw new Error('Нет данных для обновления');
|
||
}
|
||
|
||
fields.push('updated_at = CURRENT_TIMESTAMP');
|
||
values.push(id);
|
||
|
||
const sql = `UPDATE upravlenie SET ${fields.join(', ')} WHERE id = ?`;
|
||
|
||
return new Promise((resolve, reject) => {
|
||
this.db.run(sql, values, function(err) {
|
||
if (err) {
|
||
reject(err);
|
||
} else {
|
||
console.log(`✅ Подключение ${id} обновлено`);
|
||
resolve({ id, changes: this.changes });
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Удаление подключения (мягкое удаление)
|
||
*/
|
||
async deleteConnection(id) {
|
||
return this.updateConnection(id, { is_active: 0 });
|
||
}
|
||
|
||
/**
|
||
* Запуск всех задач синхронизации
|
||
*/
|
||
async startAllSyncJobs() {
|
||
const connections = await this.getAllConnections({
|
||
is_active: true,
|
||
sync_enabled: true
|
||
});
|
||
|
||
for (const conn of connections) {
|
||
this.startSyncJob(conn.id, conn.sync_interval);
|
||
}
|
||
|
||
console.log(`✅ Запущено ${connections.length} задач синхронизации`);
|
||
}
|
||
|
||
/**
|
||
* Запуск задачи синхронизации для конкретного подключения
|
||
*/
|
||
startSyncJob(connectionId, intervalMinutes) {
|
||
this.stopSyncJob(connectionId);
|
||
|
||
const intervalMs = intervalMinutes * 60 * 1000;
|
||
const interval = setInterval(() => {
|
||
this.syncConnection(connectionId);
|
||
}, intervalMs);
|
||
|
||
this.syncIntervals.set(connectionId, interval);
|
||
console.log(`✅ Запущена синхронизация для подключения ${connectionId} (интервал: ${intervalMinutes} мин)`);
|
||
|
||
// Выполняем первую синхронизацию сразу
|
||
setTimeout(() => {
|
||
this.syncConnection(connectionId);
|
||
}, 1000);
|
||
}
|
||
|
||
/**
|
||
* Остановка задачи синхронизации
|
||
*/
|
||
stopSyncJob(connectionId) {
|
||
if (this.syncIntervals.has(connectionId)) {
|
||
clearInterval(this.syncIntervals.get(connectionId));
|
||
this.syncIntervals.delete(connectionId);
|
||
console.log(`✅ Остановлена синхронизация для подключения ${connectionId}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Перезапуск задачи синхронизации
|
||
*/
|
||
async restartSyncJob(connectionId) {
|
||
this.stopSyncJob(connectionId);
|
||
|
||
const connection = await this.getConnection(connectionId);
|
||
if (connection && connection.is_active && connection.sync_enabled) {
|
||
this.startSyncJob(connectionId, connection.sync_interval);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Синхронизация с подключенным сервисом
|
||
*/
|
||
async syncConnection(connectionId) {
|
||
// Предотвращаем одновременную синхронизацию одного подключения
|
||
if (this.syncInProgress.has(connectionId)) {
|
||
console.log(`⚠️ Синхронизация подключения ${connectionId} уже выполняется`);
|
||
return;
|
||
}
|
||
|
||
this.syncInProgress.add(connectionId);
|
||
|
||
try {
|
||
const connection = await this.getConnection(connectionId);
|
||
if (!connection || !connection.is_active || !connection.sync_enabled) {
|
||
return;
|
||
}
|
||
|
||
console.log(`🔄 Синхронизация подключения ${connection.service_name} (${connection.service_id})...`);
|
||
|
||
// Обновляем время последней синхронизации
|
||
await this.updateSyncStatus(connectionId, 'in_progress');
|
||
|
||
if (connection.service_type === 'organizer') {
|
||
// Для организатора: получаем статусы от исполнителей
|
||
await this.syncFromExecutors(connection);
|
||
} else {
|
||
// Для исполнителя: отправляем статусы организатору
|
||
await this.syncToOrganizer(connection);
|
||
}
|
||
|
||
await this.updateSyncStatus(connectionId, 'success');
|
||
console.log(`✅ Синхронизация подключения ${connection.service_name} завершена`);
|
||
|
||
} catch (error) {
|
||
console.error(`❌ Ошибка синхронизации подключения ${connectionId}:`, error.message);
|
||
await this.updateSyncStatus(connectionId, 'error', error.message);
|
||
} finally {
|
||
this.syncInProgress.delete(connectionId);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Обновление статуса синхронизации
|
||
*/
|
||
updateSyncStatus(connectionId, status, error = null) {
|
||
return new Promise((resolve, reject) => {
|
||
this.db.run(
|
||
`UPDATE upravlenie
|
||
SET last_sync_at = CURRENT_TIMESTAMP,
|
||
last_sync_status = ?,
|
||
last_sync_error = ?
|
||
WHERE id = ?`,
|
||
[status, error, connectionId],
|
||
(err) => {
|
||
if (err) reject(err);
|
||
else resolve();
|
||
}
|
||
);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Синхронизация от исполнителя к организатору
|
||
*/
|
||
async syncToOrganizer(connection) {
|
||
if (!connection.api_url) {
|
||
throw new Error('Для исполнителя не указан api_url организатора');
|
||
}
|
||
|
||
// Проверяем наличие локального пользователя
|
||
if (!connection.local_user_id) {
|
||
console.warn(`⚠️ Для подключения ${connection.service_name} не указан локальный пользователь. Задачи не будут создаваться.`);
|
||
return;
|
||
}
|
||
|
||
// Получаем задачи, назначенные локальному пользователю
|
||
const localTasks = await this.getTasksForLocalUser(connection.local_user_id);
|
||
|
||
// Получаем задачи от организатора
|
||
const organizerTasks = await this.fetchTasksFromOrganizer(connection);
|
||
|
||
if (organizerTasks.length === 0) {
|
||
console.log(`📭 Нет новых задач от организатора для ${connection.service_name}`);
|
||
return;
|
||
}
|
||
|
||
// Обновляем локальные задачи
|
||
await this.syncTasksWithOrganizer(connection, organizerTasks, localTasks);
|
||
|
||
// Отправляем обновленные статусы организатору
|
||
await this.sendTaskStatusesToOrganizer(connection, localTasks);
|
||
}
|
||
|
||
/**
|
||
* Синхронизация от организатора к исполнителям
|
||
*/
|
||
async syncFromExecutors(connection) {
|
||
// Получаем все активные подключения исполнителей с таким же service_id
|
||
const executors = await this.getAllConnections({
|
||
service_type: 'executor',
|
||
is_active: true,
|
||
sync_enabled: true
|
||
});
|
||
|
||
if (executors.length === 0) {
|
||
console.log(`📭 Нет активных исполнителей для организатора ${connection.service_name}`);
|
||
return;
|
||
}
|
||
|
||
for (const executor of executors) {
|
||
try {
|
||
// Проверяем наличие локального пользователя у исполнителя
|
||
if (!executor.local_user_id) {
|
||
console.warn(`⚠️ Для исполнителя ${executor.service_name} не указан локальный пользователь. Пропускаем...`);
|
||
continue;
|
||
}
|
||
|
||
// Получаем статусы задач от исполнителя
|
||
const taskStatuses = await this.fetchTaskStatusesFromExecutor(executor);
|
||
|
||
if (taskStatuses.length > 0) {
|
||
// Обновляем статусы в локальной БД
|
||
await this.updateTaskStatusesFromExecutor(executor, taskStatuses);
|
||
console.log(`✅ Получено ${taskStatuses.length} статусов от исполнителя ${executor.service_name}`);
|
||
}
|
||
|
||
} catch (error) {
|
||
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) {
|
||
return new Promise((resolve, reject) => {
|
||
if (!userId) {
|
||
return resolve([]);
|
||
}
|
||
|
||
this.db.all(`
|
||
SELECT t.*, ta.status as assignment_status, ta.due_date as assignment_due_date
|
||
FROM tasks t
|
||
JOIN task_assignments ta ON t.id = ta.task_id
|
||
WHERE ta.user_id = ? AND t.status = 'active' AND t.closed_at IS NULL
|
||
ORDER BY t.created_at DESC
|
||
`, [userId], (err, rows) => {
|
||
if (err) reject(err);
|
||
else resolve(rows);
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Получение задач от организатора через API
|
||
*/
|
||
async fetchTasksFromOrganizer(connection) {
|
||
try {
|
||
const response = await axios.get(`${connection.api_url}/api/external/tasks`, {
|
||
params: {
|
||
service_id: connection.service_id,
|
||
user_login: connection.local_user_login
|
||
},
|
||
auth: {
|
||
username: connection.login,
|
||
password: connection.password
|
||
},
|
||
timeout: 10000
|
||
});
|
||
|
||
return response.data.tasks || [];
|
||
} catch (error) {
|
||
console.error(`❌ Ошибка получения задач от организатора:`, error.message);
|
||
throw new Error(`Не удалось получить задачи: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Получение статусов задач от исполнителя
|
||
*/
|
||
async fetchTaskStatusesFromExecutor(executor) {
|
||
try {
|
||
const response = await axios.get(`${executor.api_url}/api/external/task-statuses`, {
|
||
params: {
|
||
service_id: executor.service_id
|
||
},
|
||
auth: {
|
||
username: executor.login,
|
||
password: executor.password
|
||
},
|
||
timeout: 10000
|
||
});
|
||
|
||
return response.data.statuses || [];
|
||
} catch (error) {
|
||
console.error(`❌ Ошибка получения статусов от исполнителя:`, error.message);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Отправка статусов задач организатору
|
||
*/
|
||
async sendTaskStatusesToOrganizer(connection, localTasks) {
|
||
const statuses = localTasks.map(task => ({
|
||
task_id: task.id,
|
||
external_task_id: task.external_task_id,
|
||
status: task.assignment_status,
|
||
completed_at: task.closed_at,
|
||
comment: task.rework_comment
|
||
}));
|
||
|
||
try {
|
||
await axios.post(`${connection.api_url}/api/external/task-statuses`, {
|
||
service_id: connection.service_id,
|
||
statuses: statuses
|
||
}, {
|
||
auth: {
|
||
username: connection.login,
|
||
password: connection.password
|
||
},
|
||
timeout: 10000
|
||
});
|
||
|
||
console.log(`✅ Отправлено ${statuses.length} статусов организатору`);
|
||
} catch (error) {
|
||
console.error(`❌ Ошибка отправки статусов организатору:`, error.message);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Синхронизация задач с организатором
|
||
*/
|
||
async syncTasksWithOrganizer(connection, organizerTasks, localTasks) {
|
||
for (const organizerTask of organizerTasks) {
|
||
const localTask = localTasks.find(t => t.external_task_id === organizerTask.id);
|
||
|
||
if (!localTask) {
|
||
// Новая задача от организатора - создаем локально
|
||
await this.createTaskFromOrganizer(connection, organizerTask);
|
||
} else {
|
||
// Существующая задача - обновляем если нужно
|
||
await this.updateTaskFromOrganizer(connection, organizerTask, localTask);
|
||
}
|
||
}
|
||
}
|
||
|
||
// upravlenie-service.js (исправленная версия метода createTaskFromOrganizer)
|
||
|
||
/**
|
||
* Создание задачи из данных организатора
|
||
*/
|
||
async createTaskFromOrganizer(connection, taskData) {
|
||
return new Promise((resolve, reject) => {
|
||
// Проверяем, что local_user_id не NULL
|
||
if (!connection.local_user_id) {
|
||
reject(new Error('local_user_id не может быть пустым. Укажите локального пользователя для создания задач.'));
|
||
return;
|
||
}
|
||
|
||
// Сначала проверяем структуру таблицы tasks
|
||
this.db.all("PRAGMA table_info(tasks)", (err, columns) => {
|
||
if (err) {
|
||
reject(err);
|
||
return;
|
||
}
|
||
|
||
const columnNames = columns.map(c => c.name);
|
||
|
||
// Формируем SQL запрос динамически
|
||
let fields = ['title', 'description', 'status', 'created_by', 'start_date', 'due_date', 'task_type'];
|
||
let placeholders = ['?', '?', '?', '?', '?', '?', '?'];
|
||
let values = [
|
||
taskData.title,
|
||
taskData.description || '',
|
||
'active',
|
||
connection.local_user_id, // теперь точно не NULL
|
||
taskData.start_date || new Date().toISOString(),
|
||
taskData.due_date || null,
|
||
'external'
|
||
];
|
||
|
||
// Добавляем external_task_id если колонка существует
|
||
if (columnNames.includes('external_task_id')) {
|
||
fields.push('external_task_id');
|
||
placeholders.push('?');
|
||
values.push(taskData.id);
|
||
}
|
||
|
||
// Добавляем external_service_id если колонка существует
|
||
if (columnNames.includes('external_service_id')) {
|
||
fields.push('external_service_id');
|
||
placeholders.push('?');
|
||
values.push(connection.service_id);
|
||
}
|
||
|
||
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) {
|
||
if (err) {
|
||
console.error('❌ Ошибка создания задачи:', err);
|
||
reject(err);
|
||
return;
|
||
}
|
||
|
||
const newTaskId = this.lastID;
|
||
|
||
// Назначаем задачу локальному пользователю
|
||
this.db.run(
|
||
`INSERT INTO task_assignments (task_id, user_id, status, start_date, due_date)
|
||
VALUES (?, ?, ?, ?, ?)`,
|
||
[
|
||
newTaskId,
|
||
connection.local_user_id,
|
||
taskData.status || 'assigned',
|
||
taskData.start_date || new Date().toISOString(),
|
||
taskData.due_date || null
|
||
],
|
||
(err) => {
|
||
if (err) {
|
||
console.error('❌ Ошибка назначения задачи:', err);
|
||
reject(err);
|
||
return;
|
||
}
|
||
|
||
console.log(`✅ Создана задача ${newTaskId} от организатора ${connection.service_name}`);
|
||
resolve(newTaskId);
|
||
}
|
||
);
|
||
});
|
||
});
|
||
});
|
||
}
|
||
/**
|
||
* Обновление задачи из данных организатора
|
||
*/
|
||
async updateTaskFromOrganizer(connection, organizerTask, localTask) {
|
||
return new Promise((resolve, reject) => {
|
||
this.db.run(
|
||
`UPDATE tasks
|
||
SET title = ?, description = ?, due_date = ?, updated_at = CURRENT_TIMESTAMP
|
||
WHERE id = ?`,
|
||
[organizerTask.title, organizerTask.description || '', organizerTask.due_date || null, localTask.id],
|
||
(err) => {
|
||
if (err) reject(err);
|
||
else {
|
||
console.log(`✅ Обновлена задача ${localTask.id} от организатора`);
|
||
resolve();
|
||
}
|
||
}
|
||
);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Обновление статусов задач от исполнителя
|
||
*/
|
||
async updateTaskStatusesFromExecutor(executor, taskStatuses) {
|
||
for (const status of taskStatuses) {
|
||
await new Promise((resolve, reject) => {
|
||
this.db.run(
|
||
`UPDATE task_assignments
|
||
SET status = ?, updated_at = CURRENT_TIMESTAMP
|
||
WHERE task_id = ? AND user_id = ?`,
|
||
[
|
||
status.status,
|
||
status.task_id,
|
||
executor.local_user_id
|
||
],
|
||
function(err) {
|
||
if (err) reject(err);
|
||
else {
|
||
if (this.changes > 0) {
|
||
console.log(`✅ Обновлен статус задачи ${status.task_id}: ${status.status}`);
|
||
}
|
||
resolve();
|
||
}
|
||
}
|
||
);
|
||
});
|
||
|
||
// Если статус "completed" - проверяем и закрываем задачу если все выполнили
|
||
if (status.status === 'completed') {
|
||
await this.checkAndCloseTaskIfAllCompleted(status.task_id);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Проверка и закрытие задачи если все исполнители выполнили
|
||
*/
|
||
async checkAndCloseTaskIfAllCompleted(taskId) {
|
||
return new Promise((resolve) => {
|
||
this.db.all(
|
||
'SELECT status FROM task_assignments WHERE task_id = ?',
|
||
[taskId],
|
||
(err, assignments) => {
|
||
if (!err && assignments && assignments.length > 0) {
|
||
const allCompleted = assignments.every(a => a.status === 'completed');
|
||
|
||
if (allCompleted) {
|
||
this.db.run(
|
||
'UPDATE tasks SET closed_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
||
[taskId],
|
||
(err) => {
|
||
if (!err) {
|
||
console.log(`✅ Задача ${taskId} автоматически закрыта`);
|
||
}
|
||
resolve();
|
||
}
|
||
);
|
||
} else {
|
||
resolve();
|
||
}
|
||
} else {
|
||
resolve();
|
||
}
|
||
}
|
||
);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Загрузка файла в удаленный сервис
|
||
*/
|
||
async uploadFileToRemote(connection, taskId, fileId) {
|
||
return new Promise((resolve, reject) => {
|
||
this.db.get(
|
||
'SELECT * FROM task_files WHERE id = ?',
|
||
[fileId],
|
||
async (err, file) => {
|
||
if (err || !file) {
|
||
reject(err || new Error('Файл не найден'));
|
||
return;
|
||
}
|
||
|
||
if (!fs.existsSync(file.file_path)) {
|
||
reject(new Error('Файл не существует на диске'));
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const formData = new FormData();
|
||
formData.append('file', fs.createReadStream(file.file_path), {
|
||
filename: file.original_name,
|
||
contentType: 'application/octet-stream'
|
||
});
|
||
formData.append('task_id', taskId);
|
||
formData.append('service_id', connection.service_id);
|
||
|
||
const response = await axios.post(
|
||
`${connection.api_url}/api/external/upload-file`,
|
||
formData,
|
||
{
|
||
headers: formData.getHeaders(),
|
||
auth: {
|
||
username: connection.login,
|
||
password: connection.password
|
||
},
|
||
timeout: 30000,
|
||
maxContentLength: Infinity,
|
||
maxBodyLength: Infinity
|
||
}
|
||
);
|
||
|
||
console.log(`✅ Файл ${file.original_name} загружен в удаленный сервис`);
|
||
resolve(response.data);
|
||
|
||
} catch (error) {
|
||
console.error(`❌ Ошибка загрузки файла в удаленный сервис:`, error.message);
|
||
reject(error);
|
||
}
|
||
}
|
||
);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Получение файла из удаленного сервиса
|
||
*/
|
||
async downloadFileFromRemote(connection, remoteFileId, localTaskId) {
|
||
try {
|
||
const response = await axios.get(
|
||
`${connection.api_url}/api/external/download-file/${remoteFileId}`,
|
||
{
|
||
params: { service_id: connection.service_id },
|
||
auth: {
|
||
username: connection.login,
|
||
password: connection.password
|
||
},
|
||
responseType: 'stream',
|
||
timeout: 30000
|
||
}
|
||
);
|
||
|
||
// Сохраняем файл локально
|
||
const { createUserTaskFolder } = require('./database');
|
||
const userFolder = createUserTaskFolder(localTaskId, connection.local_user_login || 'external');
|
||
|
||
const fileName = response.headers['x-file-name'] || `remote_${Date.now()}.bin`;
|
||
const filePath = path.join(userFolder, fileName);
|
||
|
||
const writer = fs.createWriteStream(filePath);
|
||
response.data.pipe(writer);
|
||
|
||
await new Promise((resolve, reject) => {
|
||
writer.on('finish', resolve);
|
||
writer.on('error', reject);
|
||
});
|
||
|
||
// Сохраняем запись в БД
|
||
const fileSize = fs.statSync(filePath).size;
|
||
|
||
return new Promise((resolve, reject) => {
|
||
this.db.run(
|
||
`INSERT INTO task_files (task_id, user_id, filename, original_name, file_path, file_size)
|
||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||
[
|
||
localTaskId,
|
||
connection.local_user_id,
|
||
fileName,
|
||
fileName,
|
||
filePath,
|
||
fileSize
|
||
],
|
||
function(err) {
|
||
if (err) reject(err);
|
||
else {
|
||
console.log(`✅ Файл ${fileName} загружен из удаленного сервиса`);
|
||
resolve({ id: this.lastID, filePath, fileName });
|
||
}
|
||
}
|
||
);
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error(`❌ Ошибка загрузки файла из удаленного сервиса:`, error.message);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Генерация токена для внешней аутентификации
|
||
*/
|
||
generateAuthToken(serviceId, login) {
|
||
const secret = process.env.EXTERNAL_API_SECRET || 'default_secret_change_me';
|
||
const timestamp = Math.floor(Date.now() / 1000);
|
||
const data = `${serviceId}:${login}:${timestamp}`;
|
||
const hash = crypto.createHmac('sha256', secret).update(data).digest('hex');
|
||
return `${timestamp}:${hash}`;
|
||
}
|
||
|
||
/**
|
||
* Проверка токена внешней аутентификации
|
||
*/
|
||
verifyAuthToken(token, serviceId, login) {
|
||
try {
|
||
const secret = process.env.EXTERNAL_API_SECRET || 'default_secret_change_me';
|
||
const [timestamp, hash] = token.split(':');
|
||
|
||
// Проверяем, что токен не старше 5 минут
|
||
const now = Math.floor(Date.now() / 1000);
|
||
if (now - parseInt(timestamp) > 300) {
|
||
return false;
|
||
}
|
||
|
||
const data = `${serviceId}:${login}:${timestamp}`;
|
||
const expectedHash = crypto.createHmac('sha256', secret).update(data).digest('hex');
|
||
|
||
return crypto.timingSafeEqual(
|
||
Buffer.from(hash, 'hex'),
|
||
Buffer.from(expectedHash, 'hex')
|
||
);
|
||
} catch (error) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Ручная синхронизация подключения
|
||
*/
|
||
async manualSync(connectionId) {
|
||
console.log(`🔄 Ручная синхронизация подключения ${connectionId}`);
|
||
await this.syncConnection(connectionId);
|
||
}
|
||
|
||
/**
|
||
* Получение статистики синхронизации
|
||
*/
|
||
async getSyncStats() {
|
||
const connections = await this.getAllConnections({ is_active: true });
|
||
|
||
const stats = {
|
||
total: connections.length,
|
||
organizers: 0,
|
||
executors: 0,
|
||
lastSyncs: [],
|
||
errors: []
|
||
};
|
||
|
||
connections.forEach(conn => {
|
||
if (conn.service_type === 'organizer') {
|
||
stats.organizers++;
|
||
} else {
|
||
stats.executors++;
|
||
}
|
||
|
||
if (conn.last_sync_at) {
|
||
stats.lastSyncs.push({
|
||
service: conn.service_name,
|
||
last_sync: conn.last_sync_at,
|
||
status: conn.last_sync_status
|
||
});
|
||
}
|
||
|
||
if (conn.last_sync_error) {
|
||
stats.errors.push({
|
||
service: conn.service_name,
|
||
error: conn.last_sync_error,
|
||
time: conn.last_sync_at
|
||
});
|
||
}
|
||
});
|
||
|
||
return stats;
|
||
}
|
||
}
|
||
|
||
// Функция для настройки API endpoints
|
||
function setupUpravlenieEndpoints(app, db) {
|
||
const upravlenieService = new UpravlenieService(db);
|
||
|
||
// Middleware для проверки аутентификации
|
||
const requireAuth = (req, res, next) => {
|
||
if (!req.session || !req.session.user) {
|
||
return res.status(401).json({ error: 'Требуется аутентификация' });
|
||
}
|
||
next();
|
||
};
|
||
|
||
// Middleware для проверки прав администратора
|
||
const requireAdmin = (req, res, next) => {
|
||
if (!req.session || !req.session.user) {
|
||
return res.status(401).json({ error: 'Требуется аутентификация' });
|
||
}
|
||
if (req.session.user.role !== 'admin') {
|
||
return res.status(403).json({ error: 'Требуются права администратора' });
|
||
}
|
||
next();
|
||
};
|
||
|
||
// ==================== ВНУТРЕННИЕ API (для администрирования) ====================
|
||
|
||
/**
|
||
* GET /api/upravlenie/connections
|
||
* Получение всех подключений
|
||
*/
|
||
app.get('/api/upravlenie/connections', requireAdmin, async (req, res) => {
|
||
try {
|
||
const filters = {
|
||
service_type: req.query.service_type,
|
||
is_active: req.query.is_active !== undefined ? req.query.is_active === 'true' : undefined,
|
||
sync_enabled: req.query.sync_enabled !== undefined ? req.query.sync_enabled === 'true' : undefined
|
||
};
|
||
|
||
const connections = await upravlenieService.getAllConnections(filters);
|
||
res.json(connections);
|
||
} catch (error) {
|
||
console.error('❌ Ошибка получения подключений:', error);
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
/**
|
||
* GET /api/upravlenie/connections/:id
|
||
* Получение подключения по ID
|
||
*/
|
||
app.get('/api/upravlenie/connections/:id', requireAdmin, async (req, res) => {
|
||
try {
|
||
const connection = await upravlenieService.getConnection(req.params.id);
|
||
if (!connection) {
|
||
return res.status(404).json({ error: 'Подключение не найдено' });
|
||
}
|
||
res.json(connection);
|
||
} catch (error) {
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
/**
|
||
* POST /api/upravlenie/connections
|
||
* Создание нового подключения
|
||
*/
|
||
app.post('/api/upravlenie/connections', requireAdmin, async (req, res) => {
|
||
try {
|
||
const connection = await upravlenieService.createConnection(req.body);
|
||
res.json({
|
||
success: true,
|
||
message: 'Подключение создано',
|
||
connection: connection
|
||
});
|
||
} catch (error) {
|
||
console.error('❌ Ошибка создания подключения:', error);
|
||
res.status(400).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
/**
|
||
* PUT /api/upravlenie/connections/:id
|
||
* Обновление подключения
|
||
*/
|
||
app.put('/api/upravlenie/connections/:id', requireAdmin, async (req, res) => {
|
||
try {
|
||
const result = await upravlenieService.updateConnection(req.params.id, req.body);
|
||
res.json({
|
||
success: true,
|
||
message: 'Подключение обновлено',
|
||
changes: result.changes
|
||
});
|
||
} catch (error) {
|
||
res.status(400).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
/**
|
||
* DELETE /api/upravlenie/connections/:id
|
||
* Удаление подключения
|
||
*/
|
||
app.delete('/api/upravlenie/connections/:id', requireAdmin, async (req, res) => {
|
||
try {
|
||
await upravlenieService.deleteConnection(req.params.id);
|
||
res.json({ success: true, message: 'Подключение удалено' });
|
||
} catch (error) {
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
/**
|
||
* POST /api/upravlenie/connections/:id/sync
|
||
* Ручная синхронизация подключения
|
||
*/
|
||
app.post('/api/upravlenie/connections/:id/sync', requireAdmin, async (req, res) => {
|
||
try {
|
||
await upravlenieService.manualSync(req.params.id);
|
||
res.json({ success: true, message: 'Синхронизация запущена' });
|
||
} catch (error) {
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
/**
|
||
* GET /api/upravlenie/stats
|
||
* Статистика синхронизации
|
||
*/
|
||
app.get('/api/upravlenie/stats', requireAdmin, async (req, res) => {
|
||
try {
|
||
const stats = await upravlenieService.getSyncStats();
|
||
res.json(stats);
|
||
} catch (error) {
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
// ==================== ВНЕШНИЕ API (для взаимодействия между сервисами) ====================
|
||
|
||
/**
|
||
* Middleware для аутентификации внешних запросов
|
||
*/
|
||
const authenticateExternal = async (req, res, next) => {
|
||
const authHeader = req.headers.authorization;
|
||
|
||
if (!authHeader || !authHeader.startsWith('Basic ')) {
|
||
return res.status(401).json({ error: 'Требуется базовая аутентификация' });
|
||
}
|
||
|
||
const base64Credentials = authHeader.split(' ')[1];
|
||
const credentials = Buffer.from(base64Credentials, 'base64').toString('ascii');
|
||
const [login, password] = credentials.split(':');
|
||
|
||
try {
|
||
// Ищем подключение по логину и паролю
|
||
const connection = await new Promise((resolve, reject) => {
|
||
db.get(
|
||
'SELECT * FROM upravlenie WHERE login = ? AND password = ? AND is_active = 1',
|
||
[login, password],
|
||
(err, row) => {
|
||
if (err) reject(err);
|
||
else resolve(row);
|
||
}
|
||
);
|
||
});
|
||
|
||
if (!connection) {
|
||
return res.status(401).json({ error: 'Неверные учетные данные' });
|
||
}
|
||
|
||
req.externalConnection = connection;
|
||
next();
|
||
} catch (error) {
|
||
console.error('❌ Ошибка аутентификации:', error);
|
||
res.status(500).json({ error: 'Ошибка аутентификации' });
|
||
}
|
||
};
|
||
|
||
/**
|
||
* GET /api/external/tasks
|
||
* Получение задач для внешнего сервиса
|
||
* (используется исполнителем для получения задач от организатора)
|
||
*/
|
||
app.get('/api/external/tasks', authenticateExternal, async (req, res) => {
|
||
try {
|
||
const { service_id, user_login } = req.query;
|
||
const connection = req.externalConnection;
|
||
|
||
// Проверяем соответствие service_id
|
||
if (parseInt(service_id) !== connection.service_id) {
|
||
return res.status(403).json({ error: 'Неверный service_id' });
|
||
}
|
||
|
||
// Если указан user_login, ищем задачи для этого пользователя
|
||
let userId = null;
|
||
if (user_login) {
|
||
const user = await new Promise((resolve) => {
|
||
db.get('SELECT id FROM users WHERE login = ?', [user_login], (err, row) => {
|
||
resolve(row);
|
||
});
|
||
});
|
||
userId = user?.id;
|
||
}
|
||
|
||
// Получаем задачи
|
||
let tasks = [];
|
||
if (userId) {
|
||
// Задачи для конкретного пользователя
|
||
tasks = await new Promise((resolve) => {
|
||
db.all(`
|
||
SELECT t.*, ta.status as assignment_status
|
||
FROM tasks t
|
||
JOIN task_assignments ta ON t.id = ta.task_id
|
||
WHERE ta.user_id = ? AND t.status = 'active' AND t.closed_at IS NULL
|
||
`, [userId], (err, rows) => {
|
||
resolve(rows || []);
|
||
});
|
||
});
|
||
} else {
|
||
// Все активные задачи
|
||
tasks = await new Promise((resolve) => {
|
||
db.all(`
|
||
SELECT t.*, GROUP_CONCAT(ta.user_id) as assigned_users
|
||
FROM tasks t
|
||
LEFT JOIN task_assignments ta ON t.id = ta.task_id
|
||
WHERE t.status = 'active' AND t.closed_at IS NULL
|
||
GROUP BY t.id
|
||
`, [], (err, rows) => {
|
||
resolve(rows || []);
|
||
});
|
||
});
|
||
}
|
||
|
||
res.json({
|
||
service_id: connection.service_id,
|
||
tasks: tasks.map(t => ({
|
||
id: t.id,
|
||
title: t.title,
|
||
description: t.description,
|
||
status: t.assignment_status || t.status,
|
||
created_at: t.created_at,
|
||
due_date: t.due_date,
|
||
assigned_users: t.assigned_users
|
||
}))
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('❌ Ошибка получения внешних задач:', error);
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
/**
|
||
* POST /api/external/task-statuses
|
||
* Получение статусов задач от исполнителя
|
||
*/
|
||
app.post('/api/external/task-statuses', authenticateExternal, async (req, res) => {
|
||
try {
|
||
const { service_id, statuses } = req.body;
|
||
const connection = req.externalConnection;
|
||
|
||
if (parseInt(service_id) !== connection.service_id) {
|
||
return res.status(403).json({ error: 'Неверный service_id' });
|
||
}
|
||
|
||
if (!Array.isArray(statuses)) {
|
||
return res.status(400).json({ error: 'statuses должен быть массивом' });
|
||
}
|
||
|
||
// Обновляем статусы в локальной БД
|
||
for (const status of statuses) {
|
||
await new Promise((resolve, reject) => {
|
||
db.run(
|
||
`UPDATE task_assignments
|
||
SET status = ?, updated_at = CURRENT_TIMESTAMP
|
||
WHERE task_id = ?`,
|
||
[status.status, status.task_id],
|
||
function(err) {
|
||
if (err) reject(err);
|
||
else resolve();
|
||
}
|
||
);
|
||
});
|
||
|
||
// Если статус "completed" - закрываем задачу если все исполнители выполнили
|
||
if (status.status === 'completed') {
|
||
await checkAndCloseTaskIfAllCompleted(db, status.task_id);
|
||
}
|
||
}
|
||
|
||
res.json({
|
||
success: true,
|
||
message: `Получено ${statuses.length} статусов`,
|
||
processed: statuses.length
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('❌ Ошибка получения статусов от исполнителя:', error);
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
/**
|
||
* GET /api/external/task-statuses
|
||
* Получение статусов задач для организатора
|
||
*/
|
||
app.get('/api/external/task-statuses', authenticateExternal, async (req, res) => {
|
||
try {
|
||
const { service_id } = req.query;
|
||
const connection = req.externalConnection;
|
||
|
||
if (parseInt(service_id) !== connection.service_id) {
|
||
return res.status(403).json({ error: 'Неверный service_id' });
|
||
}
|
||
|
||
// Получаем задачи, созданные для этого внешнего сервиса
|
||
const statuses = await new Promise((resolve) => {
|
||
db.all(`
|
||
SELECT
|
||
t.id as task_id,
|
||
t.external_task_id,
|
||
ta.status,
|
||
t.closed_at as completed_at,
|
||
t.rework_comment as comment
|
||
FROM tasks t
|
||
JOIN task_assignments ta ON t.id = ta.task_id
|
||
WHERE t.external_service_id = ?
|
||
`, [service_id], (err, rows) => {
|
||
resolve(rows || []);
|
||
});
|
||
});
|
||
|
||
res.json({
|
||
service_id: connection.service_id,
|
||
statuses: statuses
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('❌ Ошибка получения статусов для организатора:', error);
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
/**
|
||
* POST /api/external/upload-file
|
||
* Загрузка файла от исполнителя
|
||
*/
|
||
app.post('/api/external/upload-file', authenticateExternal, async (req, res) => {
|
||
if (!req.files || !req.files.file) {
|
||
return res.status(400).json({ error: 'Файл не загружен' });
|
||
}
|
||
|
||
try {
|
||
const { task_id, service_id } = req.body;
|
||
const file = req.files.file;
|
||
const connection = req.externalConnection;
|
||
|
||
if (parseInt(service_id) !== connection.service_id) {
|
||
return res.status(403).json({ error: 'Неверный service_id' });
|
||
}
|
||
|
||
// Создаем папку для задачи
|
||
const { createUserTaskFolder } = require('./database');
|
||
const userFolder = createUserTaskFolder(task_id, connection.local_user_login || 'external');
|
||
|
||
// Сохраняем файл
|
||
const fileName = `${Date.now()}_${file.name}`;
|
||
const filePath = path.join(userFolder, fileName);
|
||
|
||
await file.mv(filePath);
|
||
|
||
// Сохраняем запись в БД
|
||
const result = await new Promise((resolve, reject) => {
|
||
db.run(
|
||
`INSERT INTO task_files (task_id, user_id, filename, original_name, file_path, file_size)
|
||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||
[
|
||
task_id,
|
||
connection.local_user_id,
|
||
fileName,
|
||
file.name,
|
||
filePath,
|
||
file.size
|
||
],
|
||
function(err) {
|
||
if (err) reject(err);
|
||
else resolve({ id: this.lastID });
|
||
}
|
||
);
|
||
});
|
||
|
||
res.json({
|
||
success: true,
|
||
file_id: result.id,
|
||
message: 'Файл успешно загружен'
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('❌ Ошибка загрузки внешнего файла:', error);
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
/**
|
||
* GET /api/external/download-file/:fileId
|
||
* Скачивание файла для исполнителя
|
||
*/
|
||
app.get('/api/external/download-file/:fileId', authenticateExternal, async (req, res) => {
|
||
try {
|
||
const { fileId } = req.params;
|
||
const { service_id } = req.query;
|
||
const connection = req.externalConnection;
|
||
|
||
if (parseInt(service_id) !== connection.service_id) {
|
||
return res.status(403).json({ error: 'Неверный service_id' });
|
||
}
|
||
|
||
// Получаем информацию о файле
|
||
const file = await new Promise((resolve, reject) => {
|
||
db.get(
|
||
'SELECT * FROM task_files WHERE id = ?',
|
||
[fileId],
|
||
(err, row) => {
|
||
if (err) reject(err);
|
||
else resolve(row);
|
||
}
|
||
);
|
||
});
|
||
|
||
if (!file) {
|
||
return res.status(404).json({ error: 'Файл не найден' });
|
||
}
|
||
|
||
if (!fs.existsSync(file.file_path)) {
|
||
return res.status(404).json({ error: 'Файл не существует на диске' });
|
||
}
|
||
|
||
// Отправляем файл
|
||
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(file.original_name)}"`);
|
||
res.setHeader('X-File-Name', encodeURIComponent(file.original_name));
|
||
res.setHeader('Content-Type', 'application/octet-stream');
|
||
|
||
fs.createReadStream(file.file_path).pipe(res);
|
||
|
||
} catch (error) {
|
||
console.error('❌ Ошибка скачивания файла:', error);
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
// Вспомогательная функция для проверки и закрытия задачи
|
||
async function checkAndCloseTaskIfAllCompleted(db, taskId) {
|
||
return new Promise((resolve) => {
|
||
db.all(
|
||
'SELECT status FROM task_assignments WHERE task_id = ?',
|
||
[taskId],
|
||
(err, assignments) => {
|
||
if (!err && assignments && assignments.length > 0) {
|
||
const allCompleted = assignments.every(a => a.status === 'completed');
|
||
|
||
if (allCompleted) {
|
||
db.run(
|
||
'UPDATE tasks SET closed_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
||
[taskId],
|
||
(err) => {
|
||
if (!err) {
|
||
console.log(`✅ Задача ${taskId} автоматически закрыта`);
|
||
}
|
||
resolve();
|
||
}
|
||
);
|
||
} else {
|
||
resolve();
|
||
}
|
||
} else {
|
||
resolve();
|
||
}
|
||
}
|
||
);
|
||
});
|
||
}
|
||
|
||
return upravlenieService;
|
||
}
|
||
|
||
module.exports = {
|
||
UpravlenieService,
|
||
setupUpravlenieEndpoints,
|
||
TaskStatus,
|
||
SyncDirection
|
||
}; |