const initSqlJs = require('sql.js'); const path = require('path'); const fs = require('fs'); const DATA_DIR = path.join(__dirname, '..', 'data'); const DB_PATH = path.join(DATA_DIR, 'exams.db'); let db = null; async function initDb() { if (!fs.existsSync(DATA_DIR)) { fs.mkdirSync(DATA_DIR, { recursive: true }); } const SQL = await initSqlJs(); if (fs.existsSync(DB_PATH)) { const buffer = fs.readFileSync(DB_PATH); db = new SQL.Database(buffer); } else { db = new SQL.Database(); } db.run('PRAGMA foreign_keys = ON'); 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, role TEXT DEFAULT 'teacher', auth_type TEXT DEFAULT 'local', groups TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, last_login DATETIME ) `); db.run(` CREATE TABLE IF NOT EXISTS students ( id INTEGER PRIMARY KEY AUTOINCREMENT, last_name TEXT NOT NULL, first_name TEXT NOT NULL, patronymic TEXT DEFAULT '', class TEXT DEFAULT '', school_code TEXT DEFAULT '', passport_series TEXT DEFAULT '', passport_number TEXT DEFAULT '', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE(last_name, first_name, patronymic, class) ) `); db.run(` CREATE TABLE IF NOT EXISTS subjects ( code TEXT PRIMARY KEY, name TEXT NOT NULL, exam_date TEXT DEFAULT '', is_mandatory INTEGER DEFAULT 0 ) `); db.run(` CREATE TABLE IF NOT EXISTS exam_results ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_id INTEGER NOT NULL, subject_code TEXT NOT NULL, primary_score INTEGER, grade INTEGER, FOREIGN KEY (student_id) REFERENCES students(id) ON DELETE CASCADE, FOREIGN KEY (subject_code) REFERENCES subjects(code), UNIQUE(student_id, subject_code) ) `); db.run('INSERT OR IGNORE INTO subjects (code, name, is_mandatory) VALUES (?, ?, 1)', ['01', 'Русский язык']); db.run('INSERT OR IGNORE INTO subjects (code, name, is_mandatory) VALUES (?, ?, 1)', ['02', 'Математика']); saveDb(); // Миграция: перенос Русского/Математики в обязательные слоты 01/02, если они пусты migrateMandatorySubjects(); console.log('БД инициализирована'); return db; } function saveDb() { if (!db) return; const data = db.export(); const buffer = Buffer.from(data); fs.writeFileSync(DB_PATH, buffer); } function migrateMandatorySubjects() { if (!db) return; // Найти русский язык с кодом ≠ 01, где слот 01 пуст (нет grade) const rusRows = dbAll(` SELECT DISTINCT er.student_id, er.subject_code, er.primary_score, er.grade FROM exam_results er JOIN subjects s ON s.code = er.subject_code WHERE s.name = 'Русский язык' AND er.subject_code != '01' AND NOT EXISTS ( SELECT 1 FROM exam_results er2 WHERE er2.student_id = er.student_id AND er2.subject_code = '01' AND er2.grade IS NOT NULL ) `); for (const row of rusRows) { dbRun('INSERT OR REPLACE INTO exam_results (student_id, subject_code, primary_score, grade) VALUES (?, ?, ?, ?)', [row.student_id, '01', row.primary_score, row.grade]); dbRun('DELETE FROM exam_results WHERE student_id = ? AND subject_code = ?', [row.student_id, row.subject_code]); } // Найти математику с кодом ≠ 02, где слот 02 пуст const mathRows = dbAll(` SELECT DISTINCT er.student_id, er.subject_code, er.primary_score, er.grade FROM exam_results er JOIN subjects s ON s.code = er.subject_code WHERE s.name = 'Математика' AND er.subject_code != '02' AND NOT EXISTS ( SELECT 1 FROM exam_results er2 WHERE er2.student_id = er.student_id AND er2.subject_code = '02' AND er2.grade IS NOT NULL ) `); for (const row of mathRows) { dbRun('INSERT OR REPLACE INTO exam_results (student_id, subject_code, primary_score, grade) VALUES (?, ?, ?, ?)', [row.student_id, '02', row.primary_score, row.grade]); dbRun('DELETE FROM exam_results WHERE student_id = ? AND subject_code = ?', [row.student_id, row.subject_code]); } // Удалить лишние subjects (не 01/02) для русского и математики, если они без результатов dbRun(`DELETE FROM subjects WHERE (name = 'Русский язык' AND code != '01') OR (name = 'Математика' AND code != '02')`); if (rusRows.length > 0 || mathRows.length > 0) { console.log(`Миграция: перенесено в обязательные — русский: ${rusRows.length}, математика: ${mathRows.length}`); saveDb(); } } function getDb() { if (!db) throw new Error('БД не инициализирована.'); return db; } // Wrappers emulating better-sqlite3 API function dbGet(sql, params = []) { const st = db.prepare(sql); st.bind(params); const hasRow = st.step(); const row = hasRow ? st.getAsObject() : null; st.free(); return row; } function dbAll(sql, params = []) { const st = db.prepare(sql); st.bind(params); const rows = []; while (st.step()) { rows.push(st.getAsObject()); } st.free(); return rows; } function dbRun(sql, params = []) { const before = db.getRowsModified(); db.run(sql, params); const after = db.getRowsModified(); return { changes: after - before }; } function dbLastInsertId() { const r = db.exec('SELECT last_insert_rowid() as id'); return r.length ? r[0].values[0][0] : null; } function dbExec(sql) { db.run(sql); } function dbBegin() { db.run('BEGIN'); } function dbCommit() { db.run('COMMIT'); } function dbRollback() { try { db.run('ROLLBACK'); } catch (e) {} } module.exports = { initDb, getDb, saveDb, dbGet, dbAll, dbRun, dbLastInsertId, dbExec, dbBegin, dbCommit, dbRollback };