feat: Implement database initialization using init.sql and update DataInitializer.
This commit is contained in:
@@ -3,61 +3,90 @@ package com.magistr.app.config;
|
|||||||
import com.magistr.app.config.tenant.TenantConfig;
|
import com.magistr.app.config.tenant.TenantConfig;
|
||||||
import com.magistr.app.config.tenant.TenantContext;
|
import com.magistr.app.config.tenant.TenantContext;
|
||||||
import com.magistr.app.config.tenant.TenantRoutingDataSource;
|
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.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.boot.CommandLineRunner;
|
import org.springframework.boot.CommandLineRunner;
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
import org.springframework.core.io.ClassPathResource;
|
||||||
import org.springframework.stereotype.Component;
|
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
|
@Component
|
||||||
public class DataInitializer implements CommandLineRunner {
|
public class DataInitializer implements CommandLineRunner {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(DataInitializer.class);
|
private static final Logger log = LoggerFactory.getLogger(DataInitializer.class);
|
||||||
|
|
||||||
private final UserRepository userRepository;
|
|
||||||
private final BCryptPasswordEncoder passwordEncoder;
|
|
||||||
private final TenantRoutingDataSource routingDataSource;
|
private final TenantRoutingDataSource routingDataSource;
|
||||||
|
private final DataSource dataSource;
|
||||||
|
|
||||||
public DataInitializer(UserRepository userRepository,
|
public DataInitializer(TenantRoutingDataSource routingDataSource, DataSource dataSource) {
|
||||||
BCryptPasswordEncoder passwordEncoder,
|
|
||||||
TenantRoutingDataSource routingDataSource) {
|
|
||||||
this.userRepository = userRepository;
|
|
||||||
this.passwordEncoder = passwordEncoder;
|
|
||||||
this.routingDataSource = routingDataSource;
|
this.routingDataSource = routingDataSource;
|
||||||
|
this.dataSource = dataSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run(String... args) {
|
public void run(String... args) {
|
||||||
// Создаём admin в каждом тенанте
|
|
||||||
for (TenantConfig tenant : routingDataSource.getTenantConfigs().values()) {
|
for (TenantConfig tenant : routingDataSource.getTenantConfigs().values()) {
|
||||||
|
String domain = tenant.getDomain();
|
||||||
try {
|
try {
|
||||||
TenantContext.setCurrentTenant(tenant.getDomain());
|
TenantContext.setCurrentTenant(domain);
|
||||||
initAdmin(tenant.getDomain());
|
|
||||||
|
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) {
|
} catch (Exception e) {
|
||||||
log.error("Failed to init admin for tenant '{}': {}", tenant.getDomain(), e.getMessage());
|
log.error("[{}] Initialization failed: {}", domain, e.getMessage());
|
||||||
} finally {
|
} finally {
|
||||||
TenantContext.clear();
|
TenantContext.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initAdmin(String tenantDomain) {
|
/**
|
||||||
Optional<User> 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();
|
* Читает init.sql из classpath и выполняет его через JDBC.
|
||||||
admin.setUsername("admin");
|
*/
|
||||||
admin.setPassword(passwordEncoder.encode("admin"));
|
private void executeInitSql() throws Exception {
|
||||||
admin.setRole(Role.ADMIN);
|
// Читаем SQL файл из ресурсов
|
||||||
userRepository.save(admin);
|
String sql;
|
||||||
log.info("[{}] Created default admin user (admin/admin)", tenantDomain);
|
try (InputStream is = new ClassPathResource("init.sql").getInputStream();
|
||||||
} else {
|
BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
|
||||||
log.info("[{}] Admin user already exists", tenantDomain);
|
sql = reader.lines().collect(Collectors.joining("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Выполняем SQL
|
||||||
|
try (Connection conn = dataSource.getConnection();
|
||||||
|
Statement stmt = conn.createStatement()) {
|
||||||
|
stmt.execute(sql);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
229
backend/src/main/resources/init.sql
Executable file
229
backend/src/main/resources/init.sql
Executable file
@@ -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 'Основное расписание занятий';
|
||||||
Reference in New Issue
Block a user