создал систему календарного учебного графика

This commit is contained in:
Zuev
2026-04-30 23:40:49 +03:00
parent 96e9d8155f
commit 89c822a073
56 changed files with 3356 additions and 1644 deletions

View File

@@ -20,13 +20,28 @@ INSERT INTO departments (name, code) VALUES
CREATE TABLE IF NOT EXISTS specialties (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
specialty_code VARCHAR(255) NOT NULL
specialty_code VARCHAR(255) UNIQUE NOT NULL
);
INSERT INTO specialties (name, specialty_code) VALUES
('Информационная безопасность', '10.03.01'),
('Информатика и вычислительная техника', '09.03.01'),
('Программная инженерия', '09.03.04');
('Программная инженерия', '09.03.04')
ON CONFLICT (specialty_code) DO NOTHING;
CREATE TABLE IF NOT EXISTS specialty_profiles (
id BIGSERIAL PRIMARY KEY,
specialty_id BIGINT NOT NULL REFERENCES specialties(id) ON DELETE CASCADE,
name VARCHAR(500) NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uq_specialty_profiles_name UNIQUE (specialty_id, name)
);
INSERT INTO specialty_profiles (specialty_id, name)
SELECT id, 'Без профиля'
FROM specialties
ON CONFLICT (specialty_id, name) DO NOTHING;
-- ==========================================
-- Пользователи и роли
@@ -70,20 +85,28 @@ ON CONFLICT (name) DO NOTHING;
-- ==========================================
CREATE TABLE IF NOT EXISTS student_groups (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) UNIQUE NOT NULL,
name VARCHAR(100) NOT NULL,
group_size BIGINT NOT NULL,
education_form_id BIGINT NOT NULL REFERENCES education_forms(id),
department_id BIGINT NOT NULL REFERENCES departments(id),
specialty_code INT NOT NULL REFERENCES specialties(id),
specialty_id BIGINT NOT NULL REFERENCES specialties(id),
specialty_profile_id BIGINT NOT NULL REFERENCES specialty_profiles(id),
year_start_study BIGINT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Тестовая базовая группа для работы
INSERT INTO student_groups (name, group_size, education_form_id, department_id, specialty_code, year_start_study)
VALUES ('ИВТ-21-1', 25, 1, 1, 2, 2025),
('ИБ-41м', 15, 2, 1, 1, 2024)
ON CONFLICT (name) DO NOTHING;
INSERT INTO student_groups (name, group_size, education_form_id, department_id, specialty_id, specialty_profile_id, year_start_study)
SELECT data.name, data.group_size, ef.id, dept.id, specialty.id, profile.id, data.year_start_study
FROM (VALUES
('ИВТ-21-1', 25::BIGINT, 'Бакалавриат', 'Кафедра ИБ', '09.03.01', 2025::BIGINT),
('ИБ-41м', 15::BIGINT, 'Магистратура', 'Кафедра ИБ', '10.03.01', 2024::BIGINT)
) AS data(name, group_size, education_form_name, department_name, specialty_code, year_start_study)
JOIN education_forms ef ON ef.name = data.education_form_name
JOIN departments dept ON dept.name = data.department_name
JOIN specialties specialty ON specialty.specialty_code = data.specialty_code
JOIN specialty_profiles profile ON profile.specialty_id = specialty.id AND profile.name = 'Без профиля'
WHERE NOT EXISTS (SELECT 1 FROM student_groups existing WHERE existing.name = data.name);
-- ==========================================
-- Подгруппы (например: "ИВТ-21-1 Подгруппа 1")
@@ -272,45 +295,131 @@ ON CONFLICT (academic_year_id, semester_type) DO NOTHING;
CREATE INDEX IF NOT EXISTS idx_semesters_dates ON semesters(start_date, end_date);
CREATE TABLE IF NOT EXISTS holidays (
CREATE TABLE IF NOT EXISTS calendar_study_forms (
id BIGSERIAL PRIMARY KEY,
date DATE NOT NULL,
name VARCHAR(100) UNIQUE NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO calendar_study_forms (name, description) VALUES
('очная', 'Очная форма обучения')
ON CONFLICT (name) DO NOTHING;
CREATE TABLE IF NOT EXISTS academic_calendar_activity_types (
id BIGSERIAL PRIMARY KEY,
code VARCHAR(10) UNIQUE NOT NULL,
name VARCHAR(255) NOT NULL,
allow_schedule BOOLEAN NOT NULL DEFAULT FALSE,
color_code VARCHAR(7) NOT NULL DEFAULT '#64748b',
display_order INT NOT NULL DEFAULT 100,
description TEXT
);
INSERT INTO academic_calendar_activity_types (code, name, allow_schedule, color_code, display_order, description) VALUES
('Т', 'Теоретическое обучение', TRUE, '#10b981', 10, 'Обычные занятия динамического расписания разрешены'),
('Э', 'Экзаменационная сессия', FALSE, '#f59e0b', 20, 'Обычные занятия не генерируются'),
('К', 'Каникулы', FALSE, '#38bdf8', 30, 'Обычные занятия не генерируются'),
('У', 'Учебная практика', FALSE, '#a78bfa', 40, 'Обычные занятия не генерируются'),
('П', 'Производственная практика', FALSE, '#fb7185', 50, 'Обычные занятия не генерируются'),
('Пд', 'Преддипломная практика', FALSE, '#f97316', 60, 'Обычные занятия не генерируются'),
('Н', 'Научно-исследовательская работа', FALSE, '#22c55e', 70, 'Обычные занятия не генерируются'),
('Г', 'Подготовка к сдаче и сдача государственного экзамена', FALSE, '#ef4444', 80, 'Обычные занятия не генерируются'),
('Д', 'Подготовка к защите и защита выпускной квалификационной работы', FALSE, '#dc2626', 90, 'Обычные занятия не генерируются'),
('ПА', 'Повторная промежуточная аттестация', FALSE, '#eab308', 100, 'Обычные занятия не генерируются'),
('С', 'Симуляционный курс', FALSE, '#06b6d4', 110, 'Обычные занятия не генерируются'),
('*', 'Нерабочий праздничный день', FALSE, '#64748b', 120, 'День исключается из генерации расписания'),
('=', 'День вне учебного года', FALSE, '#475569', 130, 'День исключается из генерации расписания')
ON CONFLICT (code) DO NOTHING;
CREATE TABLE IF NOT EXISTS academic_calendars (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
academic_year_id BIGINT NOT NULL REFERENCES academic_years(id) ON DELETE CASCADE,
description VARCHAR(255),
CONSTRAINT uq_holidays_year_date UNIQUE (academic_year_id, date)
);
CREATE INDEX IF NOT EXISTS idx_holidays_date ON holidays(date);
CREATE TABLE IF NOT EXISTS academic_calendar_matrix (
id BIGSERIAL PRIMARY KEY,
semester_id BIGINT NOT NULL REFERENCES semesters(id) ON DELETE CASCADE,
course_number INT NOT NULL,
specialty_id BIGINT NOT NULL REFERENCES specialties(id),
week_number INT NOT NULL,
activity_type VARCHAR(20) NOT NULL,
CONSTRAINT chk_calendar_course_positive CHECK (course_number > 0),
CONSTRAINT chk_calendar_week_positive CHECK (week_number > 0),
CONSTRAINT chk_calendar_activity_type CHECK (activity_type IN ('THEORY', 'EXAM', 'VACATION', 'PRACTICE')),
CONSTRAINT uq_calendar_matrix UNIQUE (semester_id, course_number, specialty_id, week_number)
specialty_profile_id BIGINT NOT NULL REFERENCES specialty_profiles(id),
study_form_id BIGINT NOT NULL REFERENCES calendar_study_forms(id),
course_count INT NOT NULL DEFAULT 4,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_academic_calendars_course_count CHECK (course_count > 0),
CONSTRAINT uq_academic_calendar_title UNIQUE (academic_year_id, specialty_profile_id, study_form_id, title)
);
CREATE INDEX IF NOT EXISTS idx_calendar_lookup
ON academic_calendar_matrix(semester_id, course_number, specialty_id, week_number);
CREATE INDEX IF NOT EXISTS idx_academic_calendars_context
ON academic_calendars(academic_year_id, specialty_id, specialty_profile_id, study_form_id);
INSERT INTO academic_calendar_matrix (semester_id, course_number, specialty_id, week_number, activity_type)
INSERT INTO academic_calendars (title, academic_year_id, specialty_id, specialty_profile_id, study_form_id, course_count)
SELECT
s.id,
SPLIT_PART(ay.title, '-', 1)::INT - sg.year_start_study + 1 AS course_number,
sg.specialty_code AS specialty_id,
weeks.week_number,
'THEORY'
FROM semesters s
JOIN academic_years ay ON ay.id = s.academic_year_id
CROSS JOIN student_groups sg
CROSS JOIN GENERATE_SERIES(1, 22) AS weeks(week_number)
WHERE SPLIT_PART(ay.title, '-', 1)::INT - sg.year_start_study + 1 > 0
ON CONFLICT (semester_id, course_number, specialty_id, week_number) DO NOTHING;
specialty.specialty_code || ' ' || profile.name || ' ' || study_form.name || ' ' || ay.title,
ay.id,
specialty.id,
profile.id,
study_form.id,
4
FROM academic_years ay
CROSS JOIN calendar_study_forms study_form
JOIN specialties specialty ON specialty.specialty_code IN ('09.03.01', '10.03.01')
JOIN specialty_profiles profile ON profile.specialty_id = specialty.id AND profile.name = 'Без профиля'
WHERE ay.title = '2025-2026'
ON CONFLICT (academic_year_id, specialty_profile_id, study_form_id, title) DO NOTHING;
CREATE TABLE IF NOT EXISTS academic_calendar_days (
id BIGSERIAL PRIMARY KEY,
calendar_id BIGINT NOT NULL REFERENCES academic_calendars(id) ON DELETE CASCADE,
course_number INT NOT NULL,
date DATE NOT NULL,
week_number INT NOT NULL,
day_of_week INT NOT NULL,
activity_type_id BIGINT NOT NULL REFERENCES academic_calendar_activity_types(id),
CONSTRAINT chk_calendar_days_course_positive CHECK (course_number > 0),
CONSTRAINT chk_calendar_days_week_positive CHECK (week_number > 0),
CONSTRAINT chk_calendar_days_day CHECK (day_of_week BETWEEN 1 AND 7),
CONSTRAINT uq_calendar_days UNIQUE (calendar_id, course_number, date)
);
CREATE INDEX IF NOT EXISTS idx_calendar_days_lookup
ON academic_calendar_days(calendar_id, course_number, date);
INSERT INTO academic_calendar_days (calendar_id, course_number, date, week_number, day_of_week, activity_type_id)
SELECT
calendar.id,
course.course_number,
days.date::DATE,
((days.date::DATE - ay.start_date) / 7) + 1,
EXTRACT(ISODOW FROM days.date::DATE)::INT,
activity.id
FROM academic_calendars calendar
JOIN academic_years ay ON ay.id = calendar.academic_year_id
JOIN academic_calendar_activity_types activity ON activity.code = 'Т'
CROSS JOIN GENERATE_SERIES(1, calendar.course_count) AS course(course_number)
CROSS JOIN GENERATE_SERIES(ay.start_date, ay.end_date, INTERVAL '1 day') AS days(date)
ON CONFLICT (calendar_id, course_number, date) DO NOTHING;
CREATE TABLE IF NOT EXISTS student_group_calendar_assignments (
id BIGSERIAL PRIMARY KEY,
group_id BIGINT NOT NULL REFERENCES student_groups(id) ON DELETE CASCADE,
academic_year_id BIGINT NOT NULL REFERENCES academic_years(id) ON DELETE CASCADE,
calendar_id BIGINT NOT NULL REFERENCES academic_calendars(id) ON DELETE CASCADE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uq_group_calendar_year UNIQUE (group_id, academic_year_id)
);
CREATE INDEX IF NOT EXISTS idx_group_calendar_assignments_lookup
ON student_group_calendar_assignments(group_id, academic_year_id);
INSERT INTO student_group_calendar_assignments (group_id, academic_year_id, calendar_id)
SELECT
sg.id,
ay.id,
calendar.id
FROM student_groups sg
JOIN academic_years ay ON ay.title = '2025-2026'
JOIN academic_calendars calendar
ON calendar.academic_year_id = ay.id
AND calendar.specialty_profile_id = sg.specialty_profile_id
WHERE sg.name IN ('ИВТ-21-1', 'ИБ-41м')
ON CONFLICT (group_id, academic_year_id) DO NOTHING;
CREATE TABLE IF NOT EXISTS schedule_rules (
id BIGSERIAL PRIMARY KEY,
@@ -445,17 +554,26 @@ CREATE TRIGGER update_users_updated_at
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_academic_calendars_updated_at
BEFORE UPDATE ON academic_calendars
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- ==========================================
-- Комментарии к таблицам и полям (для документации)
-- ==========================================
COMMENT ON TABLE users IS 'Пользователи системы (студенты, преподаватели, администраторы)';
COMMENT ON TABLE departments IS 'Кафедры';
COMMENT ON TABLE specialties IS 'Специальности';
COMMENT ON TABLE specialty_profiles IS 'Профили обучения внутри специальности';
COMMENT ON TABLE time_slots IS 'Настраиваемая сетка временных слотов занятий';
COMMENT ON TABLE academic_years IS 'Учебные годы';
COMMENT ON TABLE semesters IS 'Семестры учебного года';
COMMENT ON TABLE holidays IS 'Праздники и неучебные даты';
COMMENT ON TABLE academic_calendar_matrix IS 'Матрица учебного графика по курсу, специальности и неделе';
COMMENT ON TABLE calendar_study_forms IS 'Формы обучения календарного учебного графика';
COMMENT ON TABLE academic_calendar_activity_types IS 'Коды активностей календарного учебного графика';
COMMENT ON TABLE academic_calendars IS 'Календарные учебные графики по профилю, форме обучения и учебному году';
COMMENT ON TABLE academic_calendar_days IS 'Дневные ячейки календарного учебного графика';
COMMENT ON TABLE student_group_calendar_assignments IS 'Назначения календарных графиков учебным группам по учебному году';
COMMENT ON TABLE schedule_rules IS 'Правила динамической генерации расписания';
COMMENT ON TABLE schedule_rule_groups IS 'Привязка правил расписания к учебным группам';
COMMENT ON TABLE schedule_rule_slots IS 'Слоты проведения занятий внутри правила расписания';
@@ -489,7 +607,8 @@ COMMENT ON COLUMN student_groups.name IS 'Название группы';
COMMENT ON COLUMN student_groups.group_size IS 'Количество студентов';
COMMENT ON COLUMN student_groups.education_form_id IS 'ID формы обучения, к которой относится группа';
COMMENT ON COLUMN student_groups.department_id IS 'ID кафедры';
COMMENT ON COLUMN student_groups.specialty_code IS 'ID специальности';
COMMENT ON COLUMN student_groups.specialty_id IS 'ID специальности';
COMMENT ON COLUMN student_groups.specialty_profile_id IS 'ID профиля обучения';
COMMENT ON COLUMN student_groups.year_start_study IS 'Год начала обучения';
COMMENT ON COLUMN student_groups.created_at IS 'Дата и время создания';
@@ -526,7 +645,7 @@ COMMENT ON COLUMN classrooms.created_at IS 'Дата и время создан
COMMENT ON COLUMN classroom_equipments.classroom_id IS 'ID аудитории';
COMMENT ON COLUMN classroom_equipments.equipment_id IS 'ID оборудования';
COMMENT ON COLUMN classroom_equipments.quantity IS 'Дата и время создания'; -- Так было в V2
COMMENT ON COLUMN classroom_equipments.quantity IS 'Количество единиц оборудования';
COMMENT ON COLUMN classroom_equipments.notes IS 'Примечания к записи';
COMMENT ON COLUMN teacher_subjects.user_id IS 'ID преподавателя';
@@ -542,6 +661,37 @@ COMMENT ON COLUMN specialties.id IS 'ID специальности';
COMMENT ON COLUMN specialties.name IS 'Название специальности';
COMMENT ON COLUMN specialties.specialty_code IS 'Код специальности';
COMMENT ON COLUMN specialty_profiles.id IS 'ID профиля обучения';
COMMENT ON COLUMN specialty_profiles.specialty_id IS 'ID специальности';
COMMENT ON COLUMN specialty_profiles.name IS 'Название профиля обучения';
COMMENT ON COLUMN specialty_profiles.description IS 'Описание профиля обучения';
COMMENT ON COLUMN calendar_study_forms.id IS 'ID формы обучения графика';
COMMENT ON COLUMN calendar_study_forms.name IS 'Название формы обучения графика';
COMMENT ON COLUMN calendar_study_forms.description IS 'Описание формы обучения графика';
COMMENT ON COLUMN academic_calendar_activity_types.code IS 'Код активности из календарного графика';
COMMENT ON COLUMN academic_calendar_activity_types.name IS 'Название активности';
COMMENT ON COLUMN academic_calendar_activity_types.allow_schedule IS 'Разрешает ли код генерацию обычного расписания';
COMMENT ON COLUMN academic_calendars.title IS 'Название календарного учебного графика';
COMMENT ON COLUMN academic_calendars.academic_year_id IS 'ID учебного года';
COMMENT ON COLUMN academic_calendars.specialty_id IS 'ID специальности';
COMMENT ON COLUMN academic_calendars.specialty_profile_id IS 'ID профиля обучения';
COMMENT ON COLUMN academic_calendars.study_form_id IS 'ID формы обучения графика';
COMMENT ON COLUMN academic_calendars.course_count IS 'Количество курсов в графике';
COMMENT ON COLUMN academic_calendar_days.calendar_id IS 'ID календарного учебного графика';
COMMENT ON COLUMN academic_calendar_days.course_number IS 'Номер курса';
COMMENT ON COLUMN academic_calendar_days.date IS 'Дата';
COMMENT ON COLUMN academic_calendar_days.week_number IS 'Номер недели в учебном году';
COMMENT ON COLUMN academic_calendar_days.day_of_week IS 'День недели ISO';
COMMENT ON COLUMN academic_calendar_days.activity_type_id IS 'ID кода активности';
COMMENT ON COLUMN student_group_calendar_assignments.group_id IS 'ID учебной группы';
COMMENT ON COLUMN student_group_calendar_assignments.academic_year_id IS 'ID учебного года';
COMMENT ON COLUMN student_group_calendar_assignments.calendar_id IS 'ID назначенного календарного графика';
COMMENT ON COLUMN teacher_lesson_types.user_id IS 'ID преподавателя';
COMMENT ON COLUMN teacher_lesson_types.subject_id IS 'ID предмета';
COMMENT ON COLUMN teacher_lesson_types.lesson_type_id IS 'ID типа занятия';