diff --git a/backend/src/main/java/com/magistr/app/config/DataInitializer.java b/backend/src/main/java/com/magistr/app/config/DataInitializer.java index bb48820..4996ca9 100755 --- a/backend/src/main/java/com/magistr/app/config/DataInitializer.java +++ b/backend/src/main/java/com/magistr/app/config/DataInitializer.java @@ -3,61 +3,90 @@ package com.magistr.app.config; import com.magistr.app.config.tenant.TenantConfig; import com.magistr.app.config.tenant.TenantContext; import com.magistr.app.config.tenant.TenantRoutingDataSource; -import com.magistr.app.model.Role; -import com.magistr.app.model.User; -import com.magistr.app.repository.UserRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.CommandLineRunner; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.core.io.ClassPathResource; import org.springframework.stereotype.Component; -import java.util.Optional; +import javax.sql.DataSource; +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.stream.Collectors; +/** + * При запуске приложения проверяет каждый тенант: + * - Если таблицы не существуют — выполняет init.sql + * - init.sql создаёт все таблицы + admin (admin/admin) + тестовые данные + */ @Component public class DataInitializer implements CommandLineRunner { private static final Logger log = LoggerFactory.getLogger(DataInitializer.class); - private final UserRepository userRepository; - private final BCryptPasswordEncoder passwordEncoder; private final TenantRoutingDataSource routingDataSource; + private final DataSource dataSource; - public DataInitializer(UserRepository userRepository, - BCryptPasswordEncoder passwordEncoder, - TenantRoutingDataSource routingDataSource) { - this.userRepository = userRepository; - this.passwordEncoder = passwordEncoder; + public DataInitializer(TenantRoutingDataSource routingDataSource, DataSource dataSource) { this.routingDataSource = routingDataSource; + this.dataSource = dataSource; } @Override public void run(String... args) { - // Создаём admin в каждом тенанте for (TenantConfig tenant : routingDataSource.getTenantConfigs().values()) { + String domain = tenant.getDomain(); try { - TenantContext.setCurrentTenant(tenant.getDomain()); - initAdmin(tenant.getDomain()); + TenantContext.setCurrentTenant(domain); + + if (needsInit()) { + log.info("[{}] Tables not found — executing init.sql...", domain); + executeInitSql(); + log.info("[{}] init.sql executed successfully", domain); + } else { + log.info("[{}] Tables already exist, skipping init", domain); + } } catch (Exception e) { - log.error("Failed to init admin for tenant '{}': {}", tenant.getDomain(), e.getMessage()); + log.error("[{}] Initialization failed: {}", domain, e.getMessage()); } finally { TenantContext.clear(); } } } - private void initAdmin(String tenantDomain) { - Optional existing = userRepository.findByUsername("admin"); + /** + * Проверяет, существует ли таблица 'users' в текущей БД тенанта. + */ + private boolean needsInit() { + try (Connection conn = dataSource.getConnection(); + ResultSet rs = conn.getMetaData().getTables(null, null, "users", new String[]{"TABLE"})) { + return !rs.next(); + } catch (Exception e) { + log.warn("Could not check tables: {}", e.getMessage()); + return true; // Если не смогли проверить — пробуем init + } + } - if (existing.isEmpty()) { - User admin = new User(); - admin.setUsername("admin"); - admin.setPassword(passwordEncoder.encode("admin")); - admin.setRole(Role.ADMIN); - userRepository.save(admin); - log.info("[{}] Created default admin user (admin/admin)", tenantDomain); - } else { - log.info("[{}] Admin user already exists", tenantDomain); + /** + * Читает init.sql из classpath и выполняет его через JDBC. + */ + private void executeInitSql() throws Exception { + // Читаем SQL файл из ресурсов + String sql; + try (InputStream is = new ClassPathResource("init.sql").getInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { + sql = reader.lines().collect(Collectors.joining("\n")); + } + + // Выполняем SQL + try (Connection conn = dataSource.getConnection(); + Statement stmt = conn.createStatement()) { + stmt.execute(sql); } } } diff --git a/backend/src/main/resources/init.sql b/backend/src/main/resources/init.sql new file mode 100755 index 0000000..3c3339b --- /dev/null +++ b/backend/src/main/resources/init.sql @@ -0,0 +1,229 @@ +-- ========================================== +-- Инициализация расширений +-- ========================================== +CREATE EXTENSION IF NOT EXISTS pgcrypto; + +-- ========================================== +-- Пользователи и роли +-- ========================================== +CREATE TABLE IF NOT EXISTS users ( + id BIGSERIAL PRIMARY KEY, + username VARCHAR(50) UNIQUE NOT NULL, + password VARCHAR(255) NOT NULL, + role VARCHAR(20) NOT NULL DEFAULT 'STUDENT', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Админ по умолчанию: admin / admin (bcrypt через pgcrypto) +INSERT INTO users (username, password, role) +VALUES ('admin', crypt('admin', gen_salt('bf', 10)), 'ADMIN'), + ('Тестовый преподаватель', '1234567890', 'TEACHER') +ON CONFLICT (username) DO NOTHING; + +-- ========================================== +-- Образовательные формы +-- ========================================== +CREATE TABLE IF NOT EXISTS education_forms ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(100) UNIQUE NOT NULL, + description TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +INSERT INTO education_forms (name) VALUES + ('Бакалавриат'), + ('Магистратура'), + ('Специалитет') +ON CONFLICT (name) DO NOTHING; + +-- ========================================== +-- Учебные группы +-- ========================================== +CREATE TABLE IF NOT EXISTS student_groups ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(100) UNIQUE NOT NULL, + group_size BIGINT NOT NULL, + education_form_id BIGINT NOT NULL REFERENCES education_forms(id), + course INT CHECK (course BETWEEN 1 AND 6), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Тестовая базовая группа для работы +INSERT INTO student_groups (name, group_size, education_form_id, course) +VALUES ('ИВТ-21-1', 25, 1, 3), + ('ИБ-41м', 15, 2, 2) +ON CONFLICT (name) DO NOTHING; + +-- ========================================== +-- Подгруппы (например: "ИВТ-21-1 Подгруппа 1") +-- ========================================== +CREATE TABLE IF NOT EXISTS subgroups ( + id BIGSERIAL PRIMARY KEY, + group_id BIGINT NOT NULL REFERENCES student_groups(id) ON DELETE CASCADE, + name VARCHAR(100) NOT NULL, + student_capacity INT, + UNIQUE(group_id, name) +); + +-- ========================================== +-- Справочники +-- ========================================== + +-- Дисциплины +CREATE TABLE IF NOT EXISTS subjects ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(200) UNIQUE NOT NULL, + code VARCHAR(20), + description TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +INSERT INTO subjects (name) VALUES + ('Высшая математика'), + ('Философия'), + ('Информатика'), + ('Базы данных'), + ('Английский язык') +ON CONFLICT (name) DO NOTHING; + +-- Типы занятий +CREATE TABLE IF NOT EXISTS lesson_types ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(50) UNIQUE NOT NULL, + color_code VARCHAR(7) DEFAULT '#3788d8', -- для цветовой индикации в календаре + duration_minutes INT DEFAULT 90 +); + +INSERT INTO lesson_types (name, color_code) VALUES + ('Лекция', '#FF6B6B'), + ('Практика', '#4ECDC4'), + ('Лабораторная работа', '#45B7D1') +ON CONFLICT (name) DO NOTHING; + +-- Оборудование +CREATE TABLE IF NOT EXISTS equipments ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(50) UNIQUE NOT NULL, + description TEXT, + inventory_number VARCHAR(50) +); + +INSERT INTO equipments (name) VALUES + ('Проектор'), + ('ПК'), + ('Лаборатория'), + ('Интерактивная доска'), + ('Документ-камера'), + ('Аудиосистема') +ON CONFLICT (name) DO NOTHING; + +-- Аудитории +CREATE TABLE IF NOT EXISTS classrooms ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(50) UNIQUE NOT NULL, + capacity INT NOT NULL CHECK (capacity > 0), + building VARCHAR(50), + floor INT, + is_available BOOLEAN DEFAULT TRUE, + description TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +INSERT INTO classrooms (name, capacity, building, floor) VALUES + ('101 Ленинская', 120, 'Главный корпус', 1), + ('202 IT Lab', 20, 'Корпус IT', 2), + ('303 Обычная', 30, 'Главный корпус', 3) +ON CONFLICT (name) DO NOTHING; + +-- Привязка оборудования к аудиториям (Many-to-Many) +CREATE TABLE IF NOT EXISTS classroom_equipments ( + classroom_id BIGINT NOT NULL REFERENCES classrooms(id) ON DELETE CASCADE, + equipment_id BIGINT NOT NULL REFERENCES equipments(id) ON DELETE CASCADE, + quantity INT DEFAULT 1 CHECK (quantity > 0), + notes TEXT, + PRIMARY KEY (classroom_id, equipment_id) +); + +-- Заполнение привязок оборудования с использованием подзапросов +INSERT INTO classroom_equipments (classroom_id, equipment_id, quantity) +SELECT c.id, e.id, + CASE + WHEN e.name = 'ПК' AND c.name = '202 IT Lab' THEN 15 + WHEN e.name = 'ПК' THEN 1 + ELSE 1 + END +FROM classrooms c, equipments e +WHERE + (c.name = '101 Ленинская' AND e.name IN ('Проектор', 'Интерактивная доска', 'Аудиосистема')) + OR (c.name = '202 IT Lab' AND e.name IN ('ПК', 'Проектор', 'Лаборатория', 'Интерактивная доска')) + OR (c.name = '303 Обычная' AND e.name IN ('Проектор')) +ON CONFLICT (classroom_id, equipment_id) DO NOTHING; + +-- ========================================== +-- Связи для преподавателей +-- ========================================== + +-- Привязка преподавателей к дисциплинам +CREATE TABLE IF NOT EXISTS teacher_subjects ( + user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + subject_id BIGINT NOT NULL REFERENCES subjects(id) ON DELETE CASCADE, + qualification_level VARCHAR(50), + experience_years INT, + PRIMARY KEY(user_id, subject_id) +); + +-- Какие типы занятий может вести преподаватель по дисциплине +CREATE TABLE IF NOT EXISTS teacher_lesson_types ( + user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + subject_id BIGINT NOT NULL REFERENCES subjects(id) ON DELETE CASCADE, + lesson_type_id BIGINT NOT NULL REFERENCES lesson_types(id) ON DELETE CASCADE, + PRIMARY KEY (user_id, subject_id, lesson_type_id) +); + +-- ========================================== +-- Основная таблица Расписания (Lessons) +-- ========================================== +CREATE TABLE IF NOT EXISTS lessons ( + id BIGSERIAL PRIMARY KEY, + teacher_id BIGINT NOT NULL REFERENCES users(id), + group_id BIGINT NOT NULL REFERENCES student_groups(id), + subject_id BIGINT NOT NULL REFERENCES subjects(id), + lesson_format VARCHAR(255) NOT NULL, + type_lesson VARCHAR(255) NOT NULL, + classroom_id BIGINT NOT NULL REFERENCES classrooms(id), + day VARCHAR(255) NOT NULL, + week VARCHAR(255) NOT NULL, + time VARCHAR(255) NOT NULL +); + +INSERT INTO lessons (teacher_id, group_id, subject_id, lesson_format, type_lesson, classroom_id, day, week, time) VALUES + (2, 1, 1, 'Очно', 'Лекция', 1, 'Понедельник', 'Верхняя', '11:40 - 13:10'), + (1, 1, 2, 'Онлайн', 'Практическая работа', 2, 'Вторник', 'Нижняя', '15:00 - 16:30'), + (2, 1, 3, 'Очно', 'Лабораторная работа', 3, 'Среда', 'Верхняя', '8:00 - 9:30'), + (1, 1, 4, 'Онлайн', 'Лекция', 1, 'Четверг', 'Нижняя', '11:40 - 13:10'), + (2, 1, 5, 'Очно', 'Практическая работа', 2, 'Пятница', 'Верхняя', '15:00 - 16:30'), + (1, 1, 3, 'Онлайн', 'Лабораторная работа', 3, 'Суббота', 'Нижняя', '8:00 - 9:30'); + +-- ========================================== +-- Функция обновления timestamp +-- ========================================== +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Триггеры для обновления updated_at +CREATE TRIGGER update_users_updated_at + BEFORE UPDATE ON users + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); + +-- ========================================== +-- Комментарии к таблицам и полям (для документации) +-- ========================================== +COMMENT ON TABLE users IS 'Пользователи системы (студенты, преподаватели, администраторы)'; +COMMENT ON TABLE lessons IS 'Основное расписание занятий'; \ No newline at end of file