Files
minicrm/database.js
2026-04-06 23:39:27 +05:00

1938 lines
79 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const sqlite3 = require('sqlite3').verbose();
const { Pool } = require('pg');
const path = require('path');
const fs = require('fs');
require('dotenv').config();
// Определяем, какую базу использовать
const USE_POSTGRES = process.env.POSTGRESQL === 'yes';
let db = null; // Основной объект базы данных
let postgresPool = null; // Пул соединений PostgreSQL
let isInitialized = false; // Флаг инициализации
const dataDir = path.join(__dirname, 'data');
const createDirIfNotExists = (dirPath) => {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
};
createDirIfNotExists(dataDir);
const dbPath = path.join(dataDir, 'school_crm.db');
const uploadsDir = path.join(dataDir, 'uploads');
const tasksDir = path.join(uploadsDir, 'tasks');
const logsDir = path.join(dataDir, 'logs');
createDirIfNotExists(uploadsDir);
createDirIfNotExists(tasksDir);
createDirIfNotExists(logsDir);
// Инициализация базы данных
async function initializeDatabase() {
console.log(`🔧 Используется ${USE_POSTGRES ? 'PostgreSQL' : 'SQLite'}`);
if (USE_POSTGRES) {
// Используем PostgreSQL
try {
postgresPool = 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: 10,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 5000,
});
// Тестируем подключение
const client = await postgresPool.connect();
await client.query('SELECT 1');
client.release();
console.log('✅ Подключение к PostgreSQL установлено');
// Создаем адаптер для PostgreSQL
db = createPostgresAdapter(postgresPool);
// Проверяем и создаем таблицы
await createPostgresTables();
isInitialized = true;
} catch (error) {
console.error('❌ Ошибка подключения к PostgreSQL:', error.message);
console.log('🔄 Пытаемся использовать SQLite как запасной вариант...');
await initializeSQLite();
}
} else {
// Используем SQLite
await initializeSQLite();
}
// Синхронизируем группы пользователей
await syncUserGroups();
return db;
}
function initializeSQLite() {
return new Promise((resolve, reject) => {
db = new sqlite3.Database(dbPath, (err) => {
if (err) {
console.error('❌ Ошибка подключения к SQLite:', err.message);
reject(err);
return;
} else {
console.log('✅ Подключение к SQLite установлено');
console.log('📁 База данных расположена:', dbPath);
// Используем serialize для последовательного выполнения
db.serialize(() => {
// Создаем основные таблицы
createSQLiteTables();
// Инициализируем таблицы для документов
initDocTables(db);
// Добавляем группы по умолчанию
addDefaultGroups();
isInitialized = true;
resolve(db);
});
}
});
});
}
function initializeSQLite() {
return new Promise((resolve, reject) => {
db = new sqlite3.Database(dbPath, (err) => {
if (err) {
console.error('❌ Ошибка подключения к SQLite:', err.message);
reject(err);
return;
} else {
console.log('✅ Подключение к SQLite установлено');
console.log('📁 База данных расположена:', dbPath);
createSQLiteTables();
isInitialized = true;
resolve(db);
}
});
});
}
// Функция для синхронизации групп пользователей из старой структуры в новую
async function syncUserGroups() {
console.log('🔄 Синхронизация групп пользователей...');
try {
// Получаем всех пользователей
const users = await new Promise((resolve, reject) => {
db.all("SELECT id, groups FROM users WHERE groups IS NOT NULL AND groups != ''", [], (err, rows) => {
if (err) reject(err);
else resolve(rows || []);
});
});
let syncedCount = 0;
for (const user of users) {
try {
let groups = [];
try {
groups = JSON.parse(user.groups);
} catch (e) {
console.log(`⚠️ Не удалось распарсить группы для пользователя ${user.id}: ${user.groups}`);
continue;
}
// Для каждой группы
for (const groupName of groups) {
if (!groupName || typeof groupName !== 'string') continue;
// Находим ID группы
const group = await new Promise((resolve, reject) => {
db.get("SELECT id FROM user_groups WHERE name = ?", [groupName.trim()], (err, row) => {
if (err) reject(err);
else resolve(row);
});
});
if (group) {
// Добавляем пользователя в группу
await new Promise((resolve, reject) => {
db.run(
`INSERT INTO user_group_memberships (user_id, group_id)
VALUES (?, ?)
ON CONFLICT (user_id, group_id) DO NOTHING`,
[user.id, group.id],
function(err) {
if (err) reject(err);
else resolve();
}
);
});
console.log(`✅ Пользователь ${user.id} добавлен в группу "${groupName}"`);
} else {
console.log(`⚠️ Группа "${groupName}" не найдена для пользователя ${user.id}`);
}
}
syncedCount++;
} catch (error) {
console.error(`❌ Ошибка синхронизации пользователя ${user.id}:`, error.message);
}
}
console.log(`✅ Синхронизировано ${syncedCount} пользователей`);
} catch (error) {
console.error('❌ Ошибка синхронизации групп:', error);
}
}
function createSQLiteTables() {
// Таблица для истории уведомлений
db.run(`CREATE TABLE IF NOT EXISTS notification_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
task_id INTEGER NOT NULL,
notification_type TEXT NOT NULL,
last_sent_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id),
FOREIGN KEY (task_id) REFERENCES tasks (id),
UNIQUE(user_id, task_id, notification_type)
)`);
// Таблица очереди email
db.run(`CREATE TABLE IF NOT EXISTS email_queue (
id INTEGER PRIMARY KEY AUTOINCREMENT,
to_email TEXT NOT NULL,
subject TEXT NOT NULL,
html_content TEXT NOT NULL,
user_id INTEGER,
task_id INTEGER,
notification_type TEXT,
retry_count INTEGER DEFAULT 0,
status TEXT DEFAULT 'pending',
error_message TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (task_id) REFERENCES tasks(id)
)`);
// Таблица настроек email (для хранения состояния блокировки)
db.run(`CREATE TABLE IF NOT EXISTS email_settings (
setting_key TEXT PRIMARY KEY,
setting_value TEXT,
spam_blocked_until DATETIME,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`);
// Основные таблицы системы
db.run(`CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
login TEXT UNIQUE NOT NULL,
password TEXT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
role TEXT DEFAULT 'teacher',
auth_type TEXT DEFAULT 'local',
groups TEXT,
description TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_login DATETIME,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`);
db.run(`CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT,
status TEXT DEFAULT 'active',
created_by INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
deleted_at DATETIME,
deleted_by INTEGER,
original_task_id INTEGER,
start_date DATETIME,
due_date DATETIME,
rework_comment TEXT,
closed_at DATETIME,
closed_by INTEGER,
task_type TEXT DEFAULT "regular",
type TEXT,
approver_group_id INTEGER,
document_id INTEGER,
document_n TEXT,
document_d TEXT,
document_a TEXT,
FOREIGN KEY (created_by) REFERENCES users (id),
FOREIGN KEY (deleted_by) REFERENCES users (id),
FOREIGN KEY (original_task_id) REFERENCES tasks (id),
FOREIGN KEY (closed_by) REFERENCES users (id)
)`);
db.run(`CREATE TABLE IF NOT EXISTS task_assignments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
status TEXT DEFAULT 'assigned',
start_date DATETIME,
due_date DATETIME,
rework_comment TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (task_id) REFERENCES tasks (id),
FOREIGN KEY (user_id) REFERENCES users (id)
)`);
db.run(`CREATE TABLE IF NOT EXISTS task_files (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
filename TEXT NOT NULL,
original_name TEXT NOT NULL,
file_path TEXT NOT NULL,
file_size INTEGER NOT NULL,
uploaded_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (task_id) REFERENCES tasks (id),
FOREIGN KEY (user_id) REFERENCES users (id)
)`);
db.run(`CREATE TABLE IF NOT EXISTS activity_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
action TEXT NOT NULL,
details TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (task_id) REFERENCES tasks (id),
FOREIGN KEY (user_id) REFERENCES users (id)
)`);
db.run(`CREATE TABLE IF NOT EXISTS notification_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
notification_key TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`);
console.log('✅ База данных SQLite инициализирована');
// Таблица для пользовательских настроек
db.run(`CREATE TABLE IF NOT EXISTS user_settings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER UNIQUE NOT NULL,
email_notifications BOOLEAN DEFAULT true,
notification_email TEXT,
telegram_notifications BOOLEAN DEFAULT false,
telegram_chat_id TEXT,
vk_notifications BOOLEAN DEFAULT false,
vk_user_id TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id)
)`);
console.log('✅ Таблица для пользовательских настроек инициализирована');
// Таблица для типов документов
db.run(`CREATE TABLE IF NOT EXISTS document_types (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
description TEXT,
template_path TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`);
// Таблица для документов
db.run(`CREATE TABLE IF NOT EXISTS documents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT,
document_type_id INTEGER NOT NULL,
status TEXT DEFAULT 'draft',
created_by INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
approved_at DATETIME,
approved_by INTEGER,
rejected_at DATETIME,
rejected_by INTEGER,
rejection_reason TEXT,
file_path TEXT,
file_name TEXT,
file_size INTEGER,
version INTEGER DEFAULT 1,
parent_document_id INTEGER,
FOREIGN KEY (document_type_id) REFERENCES document_types (id),
FOREIGN KEY (created_by) REFERENCES users (id),
FOREIGN KEY (approved_by) REFERENCES users (id),
FOREIGN KEY (rejected_by) REFERENCES users (id),
FOREIGN KEY (parent_document_id) REFERENCES documents (id)
)`);
// Таблица для этапов согласования
db.run(`CREATE TABLE IF NOT EXISTS approval_stages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
document_type_id INTEGER NOT NULL,
stage_number INTEGER NOT NULL,
stage_name TEXT NOT NULL,
approver_role TEXT,
approver_user_id INTEGER,
is_required BOOLEAN DEFAULT true,
can_edit BOOLEAN DEFAULT false,
can_comment BOOLEAN DEFAULT true,
deadline_days INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(document_type_id, stage_number),
FOREIGN KEY (document_type_id) REFERENCES document_types (id),
FOREIGN KEY (approver_user_id) REFERENCES users (id)
)`);
// Таблица для согласования документов
db.run(`CREATE TABLE IF NOT EXISTS document_approvals (
id INTEGER PRIMARY KEY AUTOINCREMENT,
document_id INTEGER NOT NULL,
stage_id INTEGER NOT NULL,
approver_user_id INTEGER NOT NULL,
status TEXT DEFAULT 'pending',
comments TEXT,
approved_at DATETIME,
rejected_at DATETIME,
deadline DATETIME,
notified_at DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(document_id, stage_id, approver_user_id),
FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE,
FOREIGN KEY (stage_id) REFERENCES approval_stages (id),
FOREIGN KEY (approver_user_id) REFERENCES users (id)
)`);
// Таблица для комментариев к документам
db.run(`CREATE TABLE IF NOT EXISTS document_comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
document_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
comment TEXT NOT NULL,
is_internal BOOLEAN DEFAULT false,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users (id)
)`);
// Таблица для истории изменений документов
db.run(`CREATE TABLE IF NOT EXISTS document_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
document_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
action TEXT NOT NULL,
changes TEXT,
version INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users (id)
)`);
console.log('✅ Таблицы для согласования документов созданы');
// Таблицы для групп пользователей
db.run(`CREATE TABLE IF NOT EXISTS user_groups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
description TEXT,
color TEXT DEFAULT '#3498db',
can_approve_documents BOOLEAN DEFAULT false,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`);
db.run(`CREATE TABLE IF NOT EXISTS user_group_memberships (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
group_id INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, group_id),
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (group_id) REFERENCES user_groups (id) ON DELETE CASCADE
)`);
console.log('✅ Таблицы для групп пользователей созданы');
// Таблица для типов документов (упрощенная версия)
db.run(`CREATE TABLE IF NOT EXISTS simple_document_types (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`);
// Таблица для документов (расширенная задача)
db.run(`CREATE TABLE IF NOT EXISTS simple_documents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id INTEGER NOT NULL,
document_type_id INTEGER,
document_number TEXT,
document_date DATE,
pages_count INTEGER,
urgency_level TEXT CHECK(urgency_level IN ('normal', 'urgent', 'very_urgent')),
comment TEXT,
refusal_reason TEXT,
FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE,
FOREIGN KEY (document_type_id) REFERENCES simple_document_types(id)
)`);
console.log('✅ Таблицы для типов документов и документов (расширенные задачи) созданы');
// Таблица для сообщений чата задач
db.run(`CREATE TABLE IF NOT EXISTS task_chat_messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
message TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
is_edited BOOLEAN DEFAULT false,
is_deleted BOOLEAN DEFAULT false,
reply_to_id INTEGER,
FOREIGN KEY (task_id) REFERENCES tasks (id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (reply_to_id) REFERENCES task_chat_messages (id) ON DELETE SET NULL
)`);
// Таблица для файлов в сообщениях
db.run(`CREATE TABLE IF NOT EXISTS task_chat_files (
id INTEGER PRIMARY KEY AUTOINCREMENT,
message_id INTEGER NOT NULL,
file_path TEXT NOT NULL,
original_name TEXT NOT NULL,
file_size INTEGER NOT NULL,
file_type TEXT,
uploaded_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (message_id) REFERENCES task_chat_messages (id) ON DELETE CASCADE
)`);
// Таблица для прочитанных сообщений
db.run(`CREATE TABLE IF NOT EXISTS task_chat_reads (
id INTEGER PRIMARY KEY AUTOINCREMENT,
message_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
read_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(message_id, user_id),
FOREIGN KEY (message_id) REFERENCES task_chat_messages (id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
)`);
// Таблица для пользовательских списков
db.run(`CREATE TABLE IF NOT EXISTS user_lists (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
name TEXT NOT NULL,
user_ids TEXT NOT NULL, -- JSON массив ID пользователей
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
)`);
console.log('✅ Таблица для сообщений чата задач созданы');
// Создаем индексы для улучшения производительности
createSQLiteIndexes();
// Запускаем проверку и обновление структуры таблиц
setTimeout(() => {
checkAndUpdateTableStructure();
}, 2000);
// Добавляем группы по умолчанию
setTimeout(() => {
addDefaultGroups();
}, 1000);
}
// Добавляем группы по умолчанию
function addDefaultGroups() {
const defaultGroups = [
{
name: 'Администрация',
description: 'Пользователи с правами администратора системы',
color: '#e74c3c',
can_approve_documents: true
},
{
name: 'Секретарь',
description: 'Группа для согласования документов',
color: '#3498db',
can_approve_documents: true
},
{
name: 'help',
description: 'Группа для получения заявок поддержки',
color: '#27ae60',
can_approve_documents: false
},
{
name: 'doc',
description: 'Группа для работы с документами',
color: '#9b59b6',
can_approve_documents: false
},
{
name: 'ahch',
description: 'Группа для AHCH задач',
color: '#e67e22',
can_approve_documents: false
}
];
defaultGroups.forEach(group => {
db.get("SELECT id FROM user_groups WHERE name = ?", [group.name], (err, existing) => {
if (err) {
console.error(`❌ Ошибка проверки группы ${group.name}:`, err.message);
return;
}
if (!existing) {
db.run(
`INSERT INTO user_groups (name, description, color, can_approve_documents)
VALUES (?, ?, ?, ?)`,
[group.name, group.description, group.color, group.can_approve_documents ? 1 : 0],
(insertErr) => {
if (insertErr) {
console.error(`❌ Ошибка создания группы ${group.name}:`, insertErr.message);
} else {
console.log(`✅ Группа "${group.name}" создана по умолчанию`);
}
}
);
}
});
});
}
function createSQLiteIndexes() {
console.log('🔧 Создаем индексы для SQLite...');
const indexes = [
// Индексы для очереди email
"CREATE INDEX IF NOT EXISTS idx_email_queue_status ON email_queue(status, created_at)",
"CREATE INDEX IF NOT EXISTS idx_email_queue_user_id ON email_queue(user_id)",
"CREATE INDEX IF NOT EXISTS idx_email_queue_task_id ON email_queue(task_id)",
// Индексы для уведомлений
"CREATE INDEX IF NOT EXISTS idx_notification_history_user_id ON notification_history(user_id)",
"CREATE INDEX IF NOT EXISTS idx_notification_history_task_id ON notification_history(task_id)",
"CREATE INDEX IF NOT EXISTS idx_notification_history_type ON notification_history(notification_type)",
"CREATE INDEX IF NOT EXISTS idx_notification_history_sent_at ON notification_history(last_sent_at)",
// Индексы для пользователей
"CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)",
"CREATE INDEX IF NOT EXISTS idx_users_role ON users(role)",
// Индексы для задач
"CREATE INDEX IF NOT EXISTS idx_tasks_created_by ON tasks(created_by)",
"CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status)",
"CREATE INDEX IF NOT EXISTS idx_tasks_task_type ON tasks(task_type)",
"CREATE INDEX IF NOT EXISTS idx_tasks_due_date ON tasks(due_date)",
"CREATE INDEX IF NOT EXISTS idx_tasks_closed_at ON tasks(closed_at)",
// Индексы для назначений задач
"CREATE INDEX IF NOT EXISTS idx_task_assignments_task_id ON task_assignments(task_id)",
"CREATE INDEX IF NOT EXISTS idx_task_assignments_user_id ON task_assignments(user_id)",
"CREATE INDEX IF NOT EXISTS idx_task_assignments_status ON task_assignments(status)",
// Индексы для файлов
"CREATE INDEX IF NOT EXISTS idx_task_files_task_id ON task_files(task_id)",
// Индексы для логов активности
"CREATE INDEX IF NOT EXISTS idx_activity_logs_task_id ON activity_logs(task_id)",
"CREATE INDEX IF NOT EXISTS idx_activity_logs_created_at ON activity_logs(created_at)",
// Индексы для настроек пользователей
"CREATE INDEX IF NOT EXISTS idx_user_settings_user_id ON user_settings(user_id)",
// Индексы для групп
"CREATE INDEX IF NOT EXISTS idx_user_groups_can_approve ON user_groups(can_approve_documents)",
"CREATE INDEX IF NOT EXISTS idx_user_group_memberships_user_id ON user_group_memberships(user_id)",
"CREATE INDEX IF NOT EXISTS idx_user_group_memberships_group_id ON user_group_memberships(group_id)",
// Индексы для простых документов
"CREATE INDEX IF NOT EXISTS idx_simple_documents_task_id ON simple_documents(task_id)",
"CREATE INDEX IF NOT EXISTS idx_simple_documents_document_number ON simple_documents(document_number)",
// Индексы для оптимизации
"CREATE INDEX IF NOT EXISTS idx_task_chat_messages_task_id ON task_chat_messages(task_id)",
"CREATE INDEX IF NOT EXISTS idx_task_chat_messages_created_at ON task_chat_messages(created_at)",
"CREATE INDEX IF NOT EXISTS idx_task_chat_files_message_id ON task_chat_files(message_id)",
"CREATE INDEX IF NOT EXISTS idx_task_chat_reads_message_id ON task_chat_reads(message_id)",
"CREATE INDEX IF NOT EXISTS idx_task_chat_reads_user_id ON task_chat_reads(user_id)"
];
indexes.forEach(indexQuery => {
db.run(indexQuery, (err) => {
if (err) {
console.error(`❌ Ошибка создания индекса: ${err.message}`);
} else {
console.log(`✅ Индекс создан: ${indexQuery.split('ON')[1]}`);
}
});
});
}
// Функция для проверки и обновления структуры таблиц
function checkAndUpdateTableStructure() {
console.log('🔍 Проверка структуры таблиц...');
// Определяем ожидаемую структуру таблиц
const tableSchemas = {
users: [
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
{ name: 'login', type: 'TEXT UNIQUE NOT NULL' },
{ name: 'password', type: 'TEXT' },
{ name: 'name', type: 'TEXT NOT NULL' },
{ name: 'email', type: 'TEXT UNIQUE NOT NULL' },
{ name: 'role', type: 'TEXT DEFAULT "teacher"' },
{ name: 'auth_type', type: 'TEXT DEFAULT "local"' },
{ name: 'groups', type: 'TEXT' },
{ name: 'description', type: 'TEXT' },
{ name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' },
{ name: 'last_login', type: 'DATETIME' },
{ name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }
],
tasks: [
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
{ name: 'title', type: 'TEXT NOT NULL' },
{ name: 'description', type: 'TEXT' },
{ name: 'status', type: 'TEXT DEFAULT "active"' },
{ name: 'created_by', type: 'INTEGER NOT NULL' },
{ name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' },
{ name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' },
{ name: 'deleted_at', type: 'DATETIME' },
{ name: 'deleted_by', type: 'INTEGER' },
{ name: 'original_task_id', type: 'INTEGER' },
{ name: 'start_date', type: 'DATETIME' },
{ name: 'due_date', type: 'DATETIME' },
{ name: 'rework_comment', type: 'TEXT' },
{ name: 'closed_at', type: 'DATETIME' },
{ name: 'closed_by', type: 'INTEGER' },
{ name: 'task_type', type: 'TEXT DEFAULT "regular"' },
{ name: 'type', type: 'TEXT' },
{ name: 'approver_group_id', type: 'INTEGER' },
{ name: 'document_id', type: 'INTEGER' },
{ name: 'document_n', type: 'TEXT' },
{ name: 'document_d', type: 'TEXT' },
{ name: 'document_a', type: 'TEXT' }
],
task_assignments: [
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
{ name: 'task_id', type: 'INTEGER NOT NULL' },
{ name: 'user_id', type: 'INTEGER NOT NULL' },
{ name: 'status', type: 'TEXT DEFAULT "assigned"' },
{ name: 'start_date', type: 'DATETIME' },
{ name: 'due_date', type: 'DATETIME' },
{ name: 'rework_comment', type: 'TEXT' },
{ name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' },
{ name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }
],
task_files: [
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
{ name: 'task_id', type: 'INTEGER NOT NULL' },
{ name: 'user_id', type: 'INTEGER NOT NULL' },
{ name: 'filename', type: 'TEXT NOT NULL' },
{ name: 'original_name', type: 'TEXT NOT NULL' },
{ name: 'file_path', type: 'TEXT NOT NULL' },
{ name: 'file_size', type: 'INTEGER NOT NULL' },
{ name: 'uploaded_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }
],
activity_logs: [
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
{ name: 'task_id', type: 'INTEGER NOT NULL' },
{ name: 'user_id', type: 'INTEGER NOT NULL' },
{ name: 'action', type: 'TEXT NOT NULL' },
{ name: 'details', type: 'TEXT' },
{ name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }
],
notification_logs: [
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
{ name: 'notification_key', type: 'TEXT NOT NULL' },
{ name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }
],
user_settings: [
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
{ name: 'user_id', type: 'INTEGER UNIQUE NOT NULL' },
{ name: 'email_notifications', type: 'BOOLEAN DEFAULT true' },
{ name: 'notification_email', type: 'TEXT' },
{ name: 'telegram_notifications', type: 'BOOLEAN DEFAULT false' },
{ name: 'telegram_chat_id', type: 'TEXT' },
{ name: 'vk_notifications', type: 'BOOLEAN DEFAULT false' },
{ name: 'vk_user_id', type: 'TEXT' },
{ name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' },
{ name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' },
{ name: 'last_chat_notification_sent_at', type: 'TIMESTAMP' }
],
notification_history: [
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
{ name: 'user_id', type: 'INTEGER NOT NULL' },
{ name: 'task_id', type: 'INTEGER NOT NULL' },
{ name: 'notification_type', type: 'TEXT NOT NULL' },
{ name: 'last_sent_at', type: 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP' },
{ name: 'created_at', type: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP' }
],
email_queue: [
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
{ name: 'to_email', type: 'TEXT NOT NULL' },
{ name: 'subject', type: 'TEXT NOT NULL' },
{ name: 'html_content', type: 'TEXT NOT NULL' },
{ name: 'user_id', type: 'INTEGER' },
{ name: 'task_id', type: 'INTEGER' },
{ name: 'notification_type', type: 'TEXT' },
{ name: 'retry_count', type: 'INTEGER DEFAULT 0' },
{ name: 'status', type: 'TEXT DEFAULT "pending"' },
{ name: 'error_message', type: 'TEXT' },
{ name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' },
{ name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }
],
email_settings: [
{ name: 'setting_key', type: 'TEXT PRIMARY KEY' },
{ name: 'setting_value', type: 'TEXT' },
{ name: 'spam_blocked_until', type: 'DATETIME' },
{ name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }
],
user_groups: [
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
{ name: 'name', type: 'TEXT NOT NULL UNIQUE' },
{ name: 'description', type: 'TEXT' },
{ name: 'color', type: 'TEXT DEFAULT "#3498db"' },
{ name: 'can_approve_documents', type: 'BOOLEAN DEFAULT false' },
{ name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' },
{ name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }
],
user_group_memberships: [
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
{ name: 'user_id', type: 'INTEGER NOT NULL' },
{ name: 'group_id', type: 'INTEGER NOT NULL' },
{ name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }
],
simple_document_types: [
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
{ name: 'name', type: 'TEXT NOT NULL' },
{ name: 'description', type: 'TEXT' },
{ name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }
],
simple_documents: [
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
{ name: 'task_id', type: 'INTEGER NOT NULL' },
{ name: 'document_type_id', type: 'INTEGER' },
{ name: 'document_number', type: 'TEXT' },
{ name: 'document_date', type: 'DATE' },
{ name: 'pages_count', type: 'INTEGER' },
{ name: 'urgency_level', type: 'TEXT CHECK(urgency_level IN (\'normal\', \'urgent\', \'very_urgent\'))' },
{ name: 'comment', type: 'TEXT' },
{ name: 'refusal_reason', type: 'TEXT' }
]
};
// Проверяем каждую таблицу
Object.entries(tableSchemas).forEach(([tableName, columns]) => {
db.all(`PRAGMA table_info(${tableName})`, (err, existingColumns) => {
if (err) {
console.error(`❌ Ошибка проверки таблицы ${tableName}:`, err.message);
return;
}
if (existingColumns.length === 0) {
console.log(`⚠️ Таблица ${tableName} не существует, создаем...`);
// Таблица будет создана автоматически при следующем запуске
return;
}
// Создаем массив имен существующих колонок
const existingColumnNames = existingColumns.map(col => col.name.toLowerCase());
// Проверяем каждую ожидаемую колонку
columns.forEach(expectedColumn => {
const expectedName = expectedColumn.name.toLowerCase();
if (!existingColumnNames.includes(expectedName)) {
console.log(`🔧 Добавляем колонку ${expectedColumn.name} в таблицу ${tableName}...`);
db.run(
`ALTER TABLE ${tableName} ADD COLUMN ${expectedColumn.name} ${expectedColumn.type}`,
(alterErr) => {
if (alterErr) {
console.error(`❌ Ошибка добавления колонки ${expectedColumn.name}:`, alterErr.message);
} else {
console.log(`✅ Колонка ${expectedColumn.name} добавлена в таблицу ${tableName}`);
}
}
);
}
});
});
});
// Создаем группу "Секретарь" по умолчанию, если её нет
setTimeout(() => {
db.get("SELECT id FROM user_groups WHERE name = 'Секретарь'", (err, group) => {
if (err || !group) {
console.log('🔧 Создаем группу "Секретарь" по умолчанию...');
db.run(
`INSERT INTO user_groups (name, description, color, can_approve_documents)
VALUES ('Секретарь', 'Группа для согласования документов', '#e74c3c', 1)`,
(insertErr) => {
if (insertErr) {
console.error('❌ Ошибка создания группы "Секретарь":', insertErr.message);
} else {
console.log('✅ Группа "Секретарь" создана по умолчанию');
}
}
);
}
});
}, 1000);
}
function createPostgresAdapter(pool) {
// Адаптер для PostgreSQL с совместимым API
return {
all: (sql, params = [], callback) => {
if (!callback && typeof params === 'function') {
callback = params;
params = [];
}
// Адаптируем SQL для PostgreSQL
const adaptedSql = adaptSQLForPostgres(sql);
pool.query(adaptedSql, params)
.then(result => callback(null, result.rows))
.catch(err => {
console.error('PostgreSQL Error (all):', err.message, 'SQL:', adaptedSql);
callback(err);
});
},
get: (sql, params = [], callback) => {
if (!callback && typeof params === 'function') {
callback = params;
params = [];
}
// Адаптируем SQL для PostgreSQL
const adaptedSql = adaptSQLForPostgres(sql);
pool.query(adaptedSql, params)
.then(result => callback(null, result.rows[0] || null))
.catch(err => {
console.error('PostgreSQL Error (get):', err.message, 'SQL:', adaptedSql);
callback(err);
});
},
run: (sql, params = [], callback) => {
if (!callback && typeof params === 'function') {
callback = params;
params = [];
}
// Адаптируем SQL для PostgreSQL
const adaptedSql = adaptSQLForPostgres(sql);
pool.query(adaptedSql, params)
.then(result => {
if (callback) {
const lastIdQuery = sql.toLowerCase().includes('insert into') ?
"SELECT lastval() as last_id" : "SELECT 0 as last_id";
if (sql.toLowerCase().includes('insert into')) {
pool.query("SELECT lastval() as last_id", [])
.then(lastIdResult => {
callback(null, {
lastID: lastIdResult.rows[0]?.last_id || null,
changes: result.rowCount || 0
});
})
.catch(err => callback(err));
} else {
callback(null, {
lastID: null,
changes: result.rowCount || 0
});
}
}
})
.catch(err => {
console.error('PostgreSQL Error (run):', err.message, 'SQL:', adaptedSql);
if (callback) callback(err);
});
},
// Для транзакций - эмуляция
serialize: (callback) => {
// В PostgreSQL транзакции обрабатываются по-другому
// Здесь просто выполняем колбэк
try {
callback();
} catch (error) {
console.error('Error in serialize:', error);
}
},
// Закрытие соединения
close: (callback) => {
pool.end()
.then(() => {
if (callback) callback(null);
})
.catch(err => {
if (callback) callback(err);
});
},
// Дополнительные методы
exec: (sql, callback) => {
pool.query(sql)
.then(() => {
if (callback) callback(null);
})
.catch(err => {
if (callback) callback(err);
});
}
};
}
function adaptSQLForPostgres(sql) {
// Адаптируем SQL запросы для PostgreSQL
let adaptedSql = sql;
// Заменяем SQLite-специфичные синтаксисы
adaptedSql = adaptedSql.replace(/AUTOINCREMENT/gi, 'SERIAL');
adaptedSql = adaptedSql.replace(/DATETIME/gi, 'TIMESTAMP');
adaptedSql = adaptedSql.replace(/INTEGER PRIMARY KEY/gi, 'SERIAL PRIMARY KEY');
adaptedSql = adaptedSql.replace(/datetime\('now'\)/gi, 'CURRENT_TIMESTAMP');
adaptedSql = adaptedSql.replace(/CURRENT_TIMESTAMP/gi, 'CURRENT_TIMESTAMP');
// Исправляем INSERT с возвратом ID
if (adaptedSql.includes('INSERT INTO') && adaptedSql.includes('RETURNING id')) {
adaptedSql = adaptedSql.replace('RETURNING id', 'RETURNING id');
}
return adaptedSql;
}
async function createPostgresTables() {
if (!USE_POSTGRES) return;
try {
const client = await postgresPool.connect();
console.log('🔧 Проверяем/создаем таблицы в PostgreSQL...');
// Таблица для истории уведомлений
await client.query(`
CREATE TABLE IF NOT EXISTS notification_history (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id),
task_id INTEGER NOT NULL REFERENCES tasks(id),
notification_type VARCHAR(50) NOT NULL,
last_sent_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, task_id, notification_type)
)
`);
// Таблица очереди email
await client.query(`
CREATE TABLE IF NOT EXISTS email_queue (
id SERIAL PRIMARY KEY,
to_email VARCHAR(500) NOT NULL,
subject VARCHAR(500) NOT NULL,
html_content TEXT NOT NULL,
user_id INTEGER REFERENCES users(id),
task_id INTEGER REFERENCES tasks(id),
notification_type VARCHAR(50),
retry_count INTEGER DEFAULT 0,
status VARCHAR(50) DEFAULT 'pending',
error_message TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
// Таблица настроек email
await client.query(`
CREATE TABLE IF NOT EXISTS email_settings (
setting_key VARCHAR(100) PRIMARY KEY,
setting_value TEXT,
spam_blocked_until TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
// Основные таблицы
await client.query(`
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
login VARCHAR(100) UNIQUE NOT NULL,
password TEXT,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
role VARCHAR(50) DEFAULT 'teacher',
auth_type VARCHAR(50) DEFAULT 'local',
groups TEXT DEFAULT '[]',
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_login TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
await client.query(`
CREATE TABLE IF NOT EXISTS tasks (
id SERIAL PRIMARY KEY,
title VARCHAR(500) NOT NULL,
description TEXT,
status VARCHAR(50) DEFAULT 'active',
created_by INTEGER NOT NULL REFERENCES users(id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP,
deleted_by INTEGER REFERENCES users(id),
original_task_id INTEGER REFERENCES tasks(id),
start_date TIMESTAMP,
due_date TIMESTAMP,
rework_comment TEXT,
closed_at TIMESTAMP,
closed_by INTEGER REFERENCES users(id),
task_type VARCHAR(50) DEFAULT 'regular',
type VARCHAR(100),
approver_group_id INTEGER,
document_id INTEGER,
document_n TEXT,
document_d TEXT,
document_a TEXT
)
`);
await client.query(`
CREATE TABLE IF NOT EXISTS task_assignments (
id SERIAL PRIMARY KEY,
task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
user_id INTEGER NOT NULL REFERENCES users(id),
status VARCHAR(50) DEFAULT 'assigned',
start_date TIMESTAMP,
due_date TIMESTAMP,
rework_comment TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
await client.query(`
CREATE TABLE IF NOT EXISTS task_files (
id SERIAL PRIMARY KEY,
task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
user_id INTEGER NOT NULL REFERENCES users(id),
filename VARCHAR(255) NOT NULL,
original_name VARCHAR(500) NOT NULL,
file_path TEXT NOT NULL,
file_size BIGINT NOT NULL,
uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
await client.query(`
CREATE TABLE IF NOT EXISTS activity_logs (
id SERIAL PRIMARY KEY,
task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
user_id INTEGER NOT NULL REFERENCES users(id),
action VARCHAR(100) NOT NULL,
details TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
await client.query(`
CREATE TABLE IF NOT EXISTS notification_logs (
id SERIAL PRIMARY KEY,
notification_key VARCHAR(500) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
// Таблица для пользовательских настроек
await client.query(`
CREATE TABLE IF NOT EXISTS user_settings (
id SERIAL PRIMARY KEY,
user_id INTEGER UNIQUE NOT NULL REFERENCES users(id),
email_notifications BOOLEAN DEFAULT true,
notification_email TEXT,
telegram_notifications BOOLEAN DEFAULT false,
telegram_chat_id TEXT,
vk_notifications BOOLEAN DEFAULT false,
vk_user_id TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
// Таблицы для групп пользователей
await client.query(`
CREATE TABLE IF NOT EXISTS user_groups (
id SERIAL PRIMARY KEY,
name VARCHAR(100) UNIQUE NOT NULL,
description TEXT,
color VARCHAR(20) DEFAULT '#3498db',
can_approve_documents BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
await client.query(`
CREATE TABLE IF NOT EXISTS user_group_memberships (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
group_id INTEGER NOT NULL REFERENCES user_groups(id) ON DELETE CASCADE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, group_id)
)
`);
// Таблицы для документов
await client.query(`
CREATE TABLE IF NOT EXISTS document_types (
id SERIAL PRIMARY KEY,
name VARCHAR(100) UNIQUE NOT NULL,
description TEXT,
template_path TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
await client.query(`
CREATE TABLE IF NOT EXISTS documents (
id SERIAL PRIMARY KEY,
title VARCHAR(500) NOT NULL,
description TEXT,
document_type_id INTEGER NOT NULL REFERENCES document_types(id),
status VARCHAR(50) DEFAULT 'draft',
created_by INTEGER NOT NULL REFERENCES users(id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
approved_at TIMESTAMP,
approved_by INTEGER REFERENCES users(id),
rejected_at TIMESTAMP,
rejected_by INTEGER REFERENCES users(id),
rejection_reason TEXT,
file_path TEXT,
file_name TEXT,
file_size BIGINT,
version INTEGER DEFAULT 1,
parent_document_id INTEGER REFERENCES documents(id)
)
`);
await client.query(`
CREATE TABLE IF NOT EXISTS approval_stages (
id SERIAL PRIMARY KEY,
document_type_id INTEGER NOT NULL REFERENCES document_types(id),
stage_number INTEGER NOT NULL,
stage_name VARCHAR(100) NOT NULL,
approver_role VARCHAR(50),
approver_user_id INTEGER REFERENCES users(id),
is_required BOOLEAN DEFAULT true,
can_edit BOOLEAN DEFAULT false,
can_comment BOOLEAN DEFAULT true,
deadline_days INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(document_type_id, stage_number)
)
`);
await client.query(`
CREATE TABLE IF NOT EXISTS document_approvals (
id SERIAL PRIMARY KEY,
document_id INTEGER NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
stage_id INTEGER NOT NULL REFERENCES approval_stages(id),
approver_user_id INTEGER NOT NULL REFERENCES users(id),
status VARCHAR(50) DEFAULT 'pending',
comments TEXT,
approved_at TIMESTAMP,
rejected_at TIMESTAMP,
deadline TIMESTAMP,
notified_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(document_id, stage_id, approver_user_id)
)
`);
await client.query(`
CREATE TABLE IF NOT EXISTS document_comments (
id SERIAL PRIMARY KEY,
document_id INTEGER NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
user_id INTEGER NOT NULL REFERENCES users(id),
comment TEXT NOT NULL,
is_internal BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
await client.query(`
CREATE TABLE IF NOT EXISTS document_history (
id SERIAL PRIMARY KEY,
document_id INTEGER NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
user_id INTEGER NOT NULL REFERENCES users(id),
action VARCHAR(100) NOT NULL,
changes TEXT,
version INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
// Таблица для типов документов (упрощенная версия)
await client.query(`
CREATE TABLE IF NOT EXISTS simple_document_types (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
// Таблица для документов (расширенная задача)
await client.query(`
CREATE TABLE IF NOT EXISTS simple_documents (
id SERIAL PRIMARY KEY,
task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
document_type_id INTEGER REFERENCES simple_document_types(id),
document_number VARCHAR(100),
document_date DATE,
pages_count INTEGER,
urgency_level VARCHAR(20) CHECK (urgency_level IN ('normal', 'urgent', 'very_urgent')),
comment TEXT,
refusal_reason TEXT
)
`);
await client.query(`
CREATE TABLE IF NOT EXISTS user_lists (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR(35) NOT NULL,
user_ids TEXT NOT NULL, -- JSON массив
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
console.log('✅ Все таблицы PostgreSQL созданы/проверены');
// Создаем индексы
await createPostgresIndexes(client);
// Добавляем группы по умолчанию для PostgreSQL
await addDefaultGroupsPostgreSQL(client);
client.release();
console.log('✅ Таблицы PostgreSQL проверены/созданы');
// Проверяем структуру PostgreSQL таблиц
await checkPostgresTableStructure();
} catch (error) {
console.error('❌ Ошибка создания таблиц PostgreSQL:', error.message);
}
}
// Добавляем группы по умолчанию для PostgreSQL
async function addDefaultGroupsPostgreSQL(client) {
const defaultGroups = [
{
name: 'Администрация',
description: 'Пользователи с правами администратора системы',
color: '#e74c3c',
can_approve_documents: true
},
{
name: 'Секретарь',
description: 'Группа для согласования документов',
color: '#3498db',
can_approve_documents: true
},
{
name: 'help',
description: 'Группа для получения заявок поддержки',
color: '#27ae60',
can_approve_documents: false
},
{
name: 'doc',
description: 'Группа для работы с документами',
color: '#9b59b6',
can_approve_documents: false
},
{
name: 'ahch',
description: 'Группа для AHCH задач',
color: '#e67e22',
can_approve_documents: false
}
];
for (const group of defaultGroups) {
try {
const checkResult = await client.query(
"SELECT id FROM user_groups WHERE name = $1",
[group.name]
);
if (checkResult.rows.length === 0) {
await client.query(
`INSERT INTO user_groups (name, description, color, can_approve_documents)
VALUES ($1, $2, $3, $4)`,
[group.name, group.description, group.color, group.can_approve_documents]
);
console.log(`✅ Группа "${group.name}" создана по умолчанию в PostgreSQL`);
}
} catch (error) {
console.error(`❌ Ошибка создания группы ${group.name}:`, error.message);
}
}
}
async function createPostgresIndexes(client) {
console.log('🔧 Создаем индексы для PostgreSQL...');
const indexes = [
// Индексы для очереди email
'CREATE INDEX IF NOT EXISTS idx_email_queue_status ON email_queue(status, created_at)',
'CREATE INDEX IF NOT EXISTS idx_email_queue_user_id ON email_queue(user_id)',
'CREATE INDEX IF NOT EXISTS idx_email_queue_task_id ON email_queue(task_id)',
// Индексы для уведомлений
'CREATE INDEX IF NOT EXISTS idx_notification_history_user_id ON notification_history(user_id)',
'CREATE INDEX IF NOT EXISTS idx_notification_history_task_id ON notification_history(task_id)',
'CREATE INDEX IF NOT EXISTS idx_notification_history_type ON notification_history(notification_type)',
'CREATE INDEX IF NOT EXISTS idx_notification_history_sent_at ON notification_history(last_sent_at)',
// Индексы для пользователей
'CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)',
'CREATE INDEX IF NOT EXISTS idx_users_role ON users(role)',
// Индексы для задач
'CREATE INDEX IF NOT EXISTS idx_tasks_created_by ON tasks(created_by)',
'CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status)',
'CREATE INDEX IF NOT EXISTS idx_tasks_task_type ON tasks(task_type)',
'CREATE INDEX IF NOT EXISTS idx_tasks_due_date ON tasks(due_date)',
'CREATE INDEX IF NOT EXISTS idx_tasks_closed_at ON tasks(closed_at)',
// Индексы для назначений задач
'CREATE INDEX IF NOT EXISTS idx_task_assignments_task_id ON task_assignments(task_id)',
'CREATE INDEX IF NOT EXISTS idx_task_assignments_user_id ON task_assignments(user_id)',
'CREATE INDEX IF NOT EXISTS idx_task_assignments_status ON task_assignments(status)',
// Индексы для файлов
'CREATE INDEX IF NOT EXISTS idx_task_files_task_id ON task_files(task_id)',
// Индексы для логов активности
'CREATE INDEX IF NOT EXISTS idx_activity_logs_task_id ON activity_logs(task_id)',
'CREATE INDEX IF NOT EXISTS idx_activity_logs_created_at ON activity_logs(created_at)',
// Индексы для настроек пользователей
'CREATE INDEX IF NOT EXISTS idx_user_settings_user_id ON user_settings(user_id)',
// Индексы для групп
'CREATE INDEX IF NOT EXISTS idx_user_groups_can_approve ON user_groups(can_approve_documents)',
'CREATE INDEX IF NOT EXISTS idx_user_group_memberships_user_id ON user_group_memberships(user_id)',
'CREATE INDEX IF NOT EXISTS idx_user_group_memberships_group_id ON user_group_memberships(group_id)',
// Индексы для простых документов
'CREATE INDEX IF NOT EXISTS idx_simple_documents_task_id ON simple_documents(task_id)',
'CREATE INDEX IF NOT EXISTS idx_simple_documents_document_number ON simple_documents(document_number)'
];
for (const indexQuery of indexes) {
try {
await client.query(indexQuery);
} catch (err) {
console.warn(`⚠️ Не удалось создать индекс: ${err.message}`);
}
}
}
// Функция для проверки структуры таблиц PostgreSQL
async function checkPostgresTableStructure() {
if (!USE_POSTGRES) return;
try {
const client = await postgresPool.connect();
console.log('🔍 Проверка структуры таблиц PostgreSQL...');
// Определяем ожидаемую структуру таблиц PostgreSQL
const tableSchemas = {
email_queue: [
{ name: 'to_email', type: 'VARCHAR(500) NOT NULL' },
{ name: 'subject', type: 'VARCHAR(500) NOT NULL' },
{ name: 'html_content', type: 'TEXT NOT NULL' },
{ name: 'user_id', type: 'INTEGER REFERENCES users(id)' },
{ name: 'task_id', type: 'INTEGER REFERENCES tasks(id)' },
{ name: 'notification_type', type: 'VARCHAR(50)' },
{ name: 'retry_count', type: 'INTEGER DEFAULT 0' },
{ name: 'status', type: 'VARCHAR(50) DEFAULT \'pending\'' },
{ name: 'error_message', type: 'TEXT' },
{ name: 'created_at', type: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP' },
{ name: 'updated_at', type: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP' }
],
email_settings: [
{ name: 'setting_key', type: 'VARCHAR(100) PRIMARY KEY' },
{ name: 'setting_value', type: 'TEXT' },
{ name: 'spam_blocked_until', type: 'TIMESTAMP' },
{ name: 'updated_at', type: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP' }
],
notification_history: [
{ name: 'user_id', type: 'INTEGER NOT NULL REFERENCES users(id)' },
{ name: 'task_id', type: 'INTEGER NOT NULL REFERENCES tasks(id)' },
{ name: 'notification_type', type: 'VARCHAR(50) NOT NULL' },
{ name: 'last_sent_at', type: 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP' },
{ name: 'created_at', type: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP' }
]
};
// Проверяем каждую таблицу
for (const [tableName, columns] of Object.entries(tableSchemas)) {
try {
// Проверяем существование таблицы
const tableExists = await client.query(
"SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = $1)",
[tableName]
);
if (!tableExists.rows[0].exists) {
console.log(`⚠️ Таблица ${tableName} не существует в PostgreSQL`);
continue;
}
// Получаем существующие колонки
const existingColumns = await client.query(`
SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_name = $1
ORDER BY ordinal_position
`, [tableName]);
const existingColumnNames = existingColumns.rows.map(col => col.column_name.toLowerCase());
// Проверяем каждую ожидаемую колонку
for (const expectedColumn of columns) {
const expectedName = expectedColumn.name.toLowerCase();
if (!existingColumnNames.includes(expectedName)) {
console.log(`🔧 Добавляем колонку ${expectedColumn.name} в таблицу PostgreSQL ${tableName}...`);
try {
await client.query(
`ALTER TABLE ${tableName} ADD COLUMN ${expectedColumn.name} ${expectedColumn.type}`
);
console.log(`✅ Колонка ${expectedColumn.name} добавлена в таблицу PostgreSQL ${tableName}`);
} catch (alterErr) {
console.error(`❌ Ошибка добавления колонки ${expectedColumn.name}:`, alterErr.message);
}
}
}
} catch (error) {
console.error(`❌ Ошибка проверки таблицы PostgreSQL ${tableName}:`, error.message);
}
}
client.release();
console.log('✅ Проверка структуры таблиц PostgreSQL завершена');
} catch (error) {
console.error('❌ Ошибка проверки структуры таблиц PostgreSQL:', error.message);
}
}
// Вспомогательные функции для работы с группами пользователей
function getUserGroups(userId, callback) {
const query = `
SELECT g.* FROM user_groups g
JOIN user_group_memberships ugm ON g.id = ugm.group_id
WHERE ugm.user_id = ?
ORDER BY g.name
`;
db.all(query, [userId], (err, groups) => {
if (err) {
console.error('❌ Ошибка получения групп пользователя:', err);
callback(err, []);
} else {
callback(null, groups || []);
}
});
}
function getGroupMembers(groupId, callback) {
const query = `
SELECT u.* FROM users u
JOIN user_group_memberships ugm ON u.id = ugm.user_id
WHERE ugm.group_id = ?
ORDER BY u.name
`;
db.all(query, [groupId], (err, members) => {
if (err) {
console.error('❌ Ошибка получения участников группы:', err);
callback(err, []);
} else {
callback(null, members || []);
}
});
}
function getApproverGroups(callback) {
const query = `
SELECT g.*, COUNT(DISTINCT ugm.user_id) as member_count
FROM user_groups g
LEFT JOIN user_group_memberships ugm ON g.id = ugm.group_id
WHERE g.can_approve_documents = true
GROUP BY g.id
ORDER BY g.name
`;
db.all(query, [], (err, groups) => {
if (err) {
console.error('❌ Ошибка получения групп согласующих:', err);
callback(err, []);
} else {
callback(null, groups || []);
}
});
}
function addUserToGroup(userId, groupId, callback) {
const query = `
INSERT INTO user_group_memberships (user_id, group_id)
VALUES (?, ?)
ON CONFLICT (user_id, group_id) DO NOTHING
`;
db.run(query, [userId, groupId], function(err) {
if (err) {
console.error('❌ Ошибка добавления пользователя в группу:', err);
callback(err);
} else {
callback(null, this.changes > 0);
}
});
}
function removeUserFromGroup(userId, groupId, callback) {
const query = `
DELETE FROM user_group_memberships
WHERE user_id = ? AND group_id = ?
`;
db.run(query, [userId, groupId], function(err) {
if (err) {
console.error('❌ Ошибка удаления пользователя из группы:', err);
callback(err);
} else {
callback(null, this.changes > 0);
}
});
}
function createTaskFolder(taskId) {
const taskFolder = path.join(tasksDir, taskId.toString());
createDirIfNotExists(taskFolder);
return taskFolder;
}
function createUserTaskFolder(taskId, userLogin) {
const taskFolder = path.join(tasksDir, taskId.toString());
const userFolder = path.join(taskFolder, userLogin);
createDirIfNotExists(userFolder);
return userFolder;
}
function saveTaskMetadata(taskId, title, description, createdBy, originalTaskId = null, startDate = null, dueDate = null) {
const taskFolder = createTaskFolder(taskId);
const metadata = {
id: taskId,
title: title,
description: description,
status: 'active',
created_by: createdBy,
original_task_id: originalTaskId,
start_date: startDate,
due_date: dueDate,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
files: []
};
const metadataPath = path.join(taskFolder, 'task.json');
fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
}
function updateTaskMetadata(taskId, updates) {
const metadataPath = path.join(tasksDir, taskId.toString(), 'task.json');
if (fs.existsSync(metadataPath)) {
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
const updatedMetadata = { ...metadata, ...updates, updated_at: new Date().toISOString() };
fs.writeFileSync(metadataPath, JSON.stringify(updatedMetadata, null, 2));
}
}
function logActivity(taskId, userId, action, details = '') {
db.run(
"INSERT INTO activity_logs (task_id, user_id, action, details) VALUES (?, ?, ?, ?)",
[taskId, userId, action, details]
);
const logEntry = `${new Date().toISOString()} - User ${userId}: ${action} - Task ${taskId} - ${details}\n`;
fs.appendFileSync(path.join(logsDir, 'activity.log'), logEntry);
}
function checkTaskAccess(userId, taskId, callback) {
db.get("SELECT role FROM users WHERE id = ?", [userId], (err, user) => {
if (err) {
callback(err, false);
return;
}
// Администратор имеет полный доступ
if (user && user.role === 'admin') {
callback(null, true);
return;
}
if (user && user.role === 'tasks') {
callback(null, true);
return;
}
db.get("SELECT status, created_by, closed_at FROM tasks WHERE id = ?", [taskId], (err, task) => {
if (err || !task) {
callback(err, false);
return;
}
if (task.closed_at && task.created_by !== userId && user.role !== 'admin') {
callback(null, false);
return;
}
const query = `
SELECT 1 FROM tasks t
WHERE t.id = ? AND (
t.created_by = ?
OR EXISTS (SELECT 1 FROM task_assignments WHERE task_id = t.id AND user_id = ?)
)
`;
db.get(query, [taskId, userId, userId], (err, row) => {
callback(err, !!row);
});
});
});
}
function checkOverdueTasks() {
const now = new Date().toISOString();
const query = `
SELECT ta.id, ta.task_id, ta.user_id, ta.status, ta.due_date
FROM task_assignments ta
JOIN tasks t ON ta.task_id = t.id
WHERE ta.due_date IS NOT NULL
AND ta.due_date < ?
AND ta.status NOT IN ('completed', 'overdue')
AND t.status = 'active'
AND t.closed_at IS NULL
`;
db.all(query, [now], (err, assignments) => {
if (err) {
console.error('Ошибка при проверке просроченных задач:', err);
return;
}
assignments.forEach(assignment => {
db.run(
"UPDATE task_assignments SET status = 'overdue' WHERE id = ?",
[assignment.id]
);
logActivity(assignment.task_id, assignment.user_id, 'STATUS_CHANGED', 'Задача просрочена');
});
});
}
setInterval(checkOverdueTasks, 60000);
// Функции для работы с простыми документами
function createSimpleDocumentType(name, description, callback) {
db.run(
"INSERT INTO simple_document_types (name, description) VALUES (?, ?)",
[name, description],
function(err) {
if (err) {
console.error('❌ Ошибка создания типа документа:', err);
callback(err);
} else {
callback(null, this.lastID);
}
}
);
}
function getSimpleDocumentTypes(callback) {
db.all("SELECT * FROM simple_document_types ORDER BY name", [], (err, types) => {
if (err) {
console.error('❌ Ошибка получения типов документов:', err);
callback(err, []);
} else {
callback(null, types || []);
}
});
}
function createSimpleDocument(taskId, documentData, callback) {
const {
document_type_id,
document_number,
document_date,
pages_count,
urgency_level,
comment,
refusal_reason
} = documentData;
const query = `
INSERT INTO simple_documents (
task_id, document_type_id, document_number, document_date,
pages_count, urgency_level, comment, refusal_reason
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`;
db.run(query, [
taskId, document_type_id, document_number, document_date,
pages_count, urgency_level, comment, refusal_reason
], function(err) {
if (err) {
console.error('❌ Ошибка создания документа:', err);
callback(err);
} else {
callback(null, this.lastID);
}
});
}
function getTaskDocuments(taskId, callback) {
const query = `
SELECT sd.*, sdt.name as document_type_name
FROM simple_documents sd
LEFT JOIN simple_document_types sdt ON sd.document_type_id = sdt.id
WHERE sd.task_id = ?
ORDER BY sd.document_date DESC, sd.id DESC
`;
db.all(query, [taskId], (err, documents) => {
if (err) {
console.error('❌ Ошибка получения документов задачи:', err);
callback(err, []);
} else {
callback(null, documents || []);
}
});
}
function updateSimpleDocument(documentId, updates, callback) {
const fields = [];
const values = [];
Object.entries(updates).forEach(([key, value]) => {
fields.push(`${key} = ?`);
values.push(value);
});
if (fields.length === 0) {
callback(new Error('Нет полей для обновления'));
return;
}
values.push(documentId);
const query = `UPDATE simple_documents SET ${fields.join(', ')} WHERE id = ?`;
db.run(query, values, function(err) {
if (err) {
console.error('❌ Ошибка обновления документа:', err);
callback(err);
} else {
callback(null, this.changes > 0);
}
});
}
function deleteSimpleDocument(documentId, callback) {
db.run("DELETE FROM simple_documents WHERE id = ?", [documentId], function(err) {
if (err) {
console.error('❌ Ошибка удаления документа:', err);
callback(err);
} else {
callback(null, this.changes > 0);
}
});
}
module.exports = {
initializeDatabase, // Экспортируем функцию инициализации
getDb: () => {
if (!isInitialized) {
throw new Error('База данных не инициализирована');
}
return db;
},
isInitialized: () => isInitialized,
logActivity,
createTaskFolder,
createUserTaskFolder,
saveTaskMetadata,
updateTaskMetadata,
checkTaskAccess,
USE_POSTGRES,
getDatabaseType: () => USE_POSTGRES ? 'PostgreSQL' : 'SQLite',
checkAndUpdateTableStructure, // Экспортируем для ручного запуска
syncUserGroups, // Экспортируем функцию синхронизации
// Функции для работы с группами
getUserGroups,
getGroupMembers,
getApproverGroups,
addUserToGroup,
removeUserFromGroup,
// Функции для работы с простыми документами
createSimpleDocumentType,
getSimpleDocumentTypes,
createSimpleDocument,
getTaskDocuments,
updateSimpleDocument,
deleteSimpleDocument
};
// Запускаем инициализацию при экспорте (но она завершится позже)
initializeDatabase().catch(err => {
console.error('❌ Ошибка инициализации базы данных:', err.message);
});