From 49fdc3111bc6dd15a63ff157843e4ff87b68ebb8 Mon Sep 17 00:00:00 2001 From: Tattaboe Date: Fri, 13 Feb 2026 11:16:07 +0400 Subject: [PATCH 1/7] Lab1: Complete --- .../Polyclinic.Domain/Entities/Appointment.cs | 53 +++ .../Polyclinic.Domain/Entities/Doctor.cs | 60 +++ .../Polyclinic.Domain/Entities/Patient.cs | 75 +++ .../Entities/Specialization.cs | 39 ++ .../Polyclinic.Domain/Enums/BloodGroup.cs | 33 ++ Polyclinic/Polyclinic.Domain/Enums/Gender.cs | 28 ++ .../Polyclinic.Domain/Enums/RhFactor.cs | 23 + .../Polyclinic.Domain.csproj | 9 + .../Polyclinic.Tests/Polyclinic.Tests.csproj | 30 ++ .../Polyclinic.Tests/PolyclinicFixture.cs | 440 ++++++++++++++++++ .../Polyclinic.Tests/PolyclinicTests.cs | 175 +++++++ Polyclinic/Polyclinic.Tests/TestConstants.cs | 47 ++ Polyclinic/Polyclinic.sln | 31 ++ README.md | 160 ++----- 14 files changed, 1080 insertions(+), 123 deletions(-) create mode 100644 Polyclinic/Polyclinic.Domain/Entities/Appointment.cs create mode 100644 Polyclinic/Polyclinic.Domain/Entities/Doctor.cs create mode 100644 Polyclinic/Polyclinic.Domain/Entities/Patient.cs create mode 100644 Polyclinic/Polyclinic.Domain/Entities/Specialization.cs create mode 100644 Polyclinic/Polyclinic.Domain/Enums/BloodGroup.cs create mode 100644 Polyclinic/Polyclinic.Domain/Enums/Gender.cs create mode 100644 Polyclinic/Polyclinic.Domain/Enums/RhFactor.cs create mode 100644 Polyclinic/Polyclinic.Domain/Polyclinic.Domain.csproj create mode 100644 Polyclinic/Polyclinic.Tests/Polyclinic.Tests.csproj create mode 100644 Polyclinic/Polyclinic.Tests/PolyclinicFixture.cs create mode 100644 Polyclinic/Polyclinic.Tests/PolyclinicTests.cs create mode 100644 Polyclinic/Polyclinic.Tests/TestConstants.cs create mode 100644 Polyclinic/Polyclinic.sln diff --git a/Polyclinic/Polyclinic.Domain/Entities/Appointment.cs b/Polyclinic/Polyclinic.Domain/Entities/Appointment.cs new file mode 100644 index 000000000..b9031c89c --- /dev/null +++ b/Polyclinic/Polyclinic.Domain/Entities/Appointment.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Polyclinic.Domain.Entities; + +/// +/// Запись пациента на прием к врачу +/// +public class Appointment +{ + /// + /// Уникальный идентификатор записи + /// + public int Id { get; set; } + + /// + /// Дата и время приема + /// + public DateTime AppointmentDateTime { get; set; } + + /// + /// Номер кабинета + /// + public required string RoomNumber { get; set; } + + /// + /// Флаг повторного приема + /// + public bool IsRepeat { get; set; } + + /// + /// Идентификатор пациента + /// + public int PatientId { get; set; } + + /// + /// Идентификатор врача + /// + public int DoctorId { get; set; } + + /// + /// Навигационное свойство: пациент + /// + public Patient? Patient { get; set; } + + /// + /// Навигационное свойство: врач + /// + public Doctor? Doctor { get; set; } +} \ No newline at end of file diff --git a/Polyclinic/Polyclinic.Domain/Entities/Doctor.cs b/Polyclinic/Polyclinic.Domain/Entities/Doctor.cs new file mode 100644 index 000000000..907e42a27 --- /dev/null +++ b/Polyclinic/Polyclinic.Domain/Entities/Doctor.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using Polyclinic.Domain.Enums; + +namespace Polyclinic.Domain.Entities; + +/// +/// Врач поликлиники +/// +public class Doctor +{ + /// + /// Уникальный идентификатор врача + /// + public int Id { get; set; } + + /// + /// Номер паспорта (уникальный) + /// + public required string PassportNumber { get; set; } + + /// + /// ФИО врача + /// + public required string FullName { get; set; } + + /// + /// Год рождения + /// + public int BirthYear { get; set; } + + /// + /// Идентификатор специализации + /// + public int SpecializationId { get; set; } + + /// + /// Стаж работы (в годах) + /// + public int ExperienceYears { get; set; } + + /// + /// Навигационное свойство: специализация + /// + public Specialization? Specialization { get; set; } + + /// + /// Список приемов у этого врача + /// + public List Appointments { get; set; } = []; + + /// + /// Вычисление возраста врача на указанную дату + /// + public int GetAge(DateTime onDate) => onDate.Year - BirthYear; +} \ No newline at end of file diff --git a/Polyclinic/Polyclinic.Domain/Entities/Patient.cs b/Polyclinic/Polyclinic.Domain/Entities/Patient.cs new file mode 100644 index 000000000..cd40dc67c --- /dev/null +++ b/Polyclinic/Polyclinic.Domain/Entities/Patient.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using Polyclinic.Domain.Enums; + +namespace Polyclinic.Domain.Entities; + +/// +/// Пациент поликлиники +/// +public class Patient +{ + /// + /// Уникальный идентификатор пациента + /// + public int Id { get; set; } + + /// + /// Номер паспорта (уникальный) + /// + public required string PassportNumber { get; set; } + + /// + /// ФИО пациента + /// + public required string FullName { get; set; } + + /// + /// Пол пациента + /// + public Gender Gender { get; set; } + + /// + /// Дата рождения + /// + public DateTime BirthDate { get; set; } + + /// + /// Адрес проживания + /// + public required string Address { get; set; } + + /// + /// Группа крови + /// + public BloodGroup BloodGroup { get; set; } + + /// + /// Резус-фактор + /// + public RhFactor RhFactor { get; set; } + + /// + /// Контактный телефон + /// + public required string PhoneNumber { get; set; } + + /// + /// Список записей на прием этого пациента + /// + public List Appointments { get; set; } = []; + + /// + /// Вычисление возраста пациента на указанную дату + /// + public int GetAge(DateTime onDate) + { + var age = onDate.Year - BirthDate.Year; + if (BirthDate.Date > onDate.AddYears(-age)) age--; + return age; + } +} diff --git a/Polyclinic/Polyclinic.Domain/Entities/Specialization.cs b/Polyclinic/Polyclinic.Domain/Entities/Specialization.cs new file mode 100644 index 000000000..9aadf9fda --- /dev/null +++ b/Polyclinic/Polyclinic.Domain/Entities/Specialization.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text; +using System.Threading.Tasks; + +namespace Polyclinic.Domain.Entities; + +/// +/// Специализация врача (справочник) +/// +public class Specialization +{ + /// + /// Уникальный идентификатор специализации + /// + public int Id { get; set; } + + /// + /// Название специализации + /// + public required string Name { get; set; } + + /// + /// Описание специализации + /// + public required string Description { get; set; } + + /// + /// Код специализации + /// + public required string Code { get; set; } + + /// + /// Список врачей с этой специализацией + /// + public List Doctors { get; set; } = []; +} diff --git a/Polyclinic/Polyclinic.Domain/Enums/BloodGroup.cs b/Polyclinic/Polyclinic.Domain/Enums/BloodGroup.cs new file mode 100644 index 000000000..10d251989 --- /dev/null +++ b/Polyclinic/Polyclinic.Domain/Enums/BloodGroup.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Polyclinic.Domain.Enums; + +/// +/// Группа крови пациента +/// +public enum BloodGroup +{ + /// + /// Первая (0) + /// + O, + + /// + /// Вторая (A) + /// + A, + + /// + /// Третья (B) + /// + B, + + /// + /// Четвертая (AB) + /// + AB +} diff --git a/Polyclinic/Polyclinic.Domain/Enums/Gender.cs b/Polyclinic/Polyclinic.Domain/Enums/Gender.cs new file mode 100644 index 000000000..09d9260a7 --- /dev/null +++ b/Polyclinic/Polyclinic.Domain/Enums/Gender.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Polyclinic.Domain.Enums; + +/// +/// Пол пациента +/// +public enum Gender +{ + /// + /// Мужской + /// + Male, + + /// + /// Женский + /// + Female, + + /// + /// Не указано + /// + Other +} diff --git a/Polyclinic/Polyclinic.Domain/Enums/RhFactor.cs b/Polyclinic/Polyclinic.Domain/Enums/RhFactor.cs new file mode 100644 index 000000000..ed4b1726e --- /dev/null +++ b/Polyclinic/Polyclinic.Domain/Enums/RhFactor.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Polyclinic.Domain.Enums; + +/// +/// Резус-фактор пациента +/// +public enum RhFactor +{ + /// + /// Положительный + /// + Positive, + + /// + /// Отрицательный + /// + Negative +} diff --git a/Polyclinic/Polyclinic.Domain/Polyclinic.Domain.csproj b/Polyclinic/Polyclinic.Domain/Polyclinic.Domain.csproj new file mode 100644 index 000000000..fa71b7ae6 --- /dev/null +++ b/Polyclinic/Polyclinic.Domain/Polyclinic.Domain.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/Polyclinic/Polyclinic.Tests/Polyclinic.Tests.csproj b/Polyclinic/Polyclinic.Tests/Polyclinic.Tests.csproj new file mode 100644 index 000000000..bb1e0508d --- /dev/null +++ b/Polyclinic/Polyclinic.Tests/Polyclinic.Tests.csproj @@ -0,0 +1,30 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/Polyclinic/Polyclinic.Tests/PolyclinicFixture.cs b/Polyclinic/Polyclinic.Tests/PolyclinicFixture.cs new file mode 100644 index 000000000..54e65d018 --- /dev/null +++ b/Polyclinic/Polyclinic.Tests/PolyclinicFixture.cs @@ -0,0 +1,440 @@ +using Polyclinic.Domain.Entities; +using Polyclinic.Domain.Enums; + +namespace Polyclinic.Tests; + +/// +/// Фикстура с тестовыми данными для поликлиники +/// +public class PolyclinicFixture +{ + /// + /// Список специализаций + /// + public List Specializations { get; } + + /// + /// Список врачей + /// + public List Doctors { get; } + + /// + /// Список пациентов + /// + public List Patients { get; } + + /// + /// Список записей на прием + /// + public List Appointments { get; } + + /// + /// Конструктор, инициализирующий тестовые данные + /// + public PolyclinicFixture() + { + Specializations = GetSpecializations(); + Doctors = GetDoctors(); + Patients = GetPatients(); + Appointments = GetAppointments(); + + LinkDoctorsWithSpecializations(); + LinkAppointmentsWithDoctorsAndPatients(); + } + + private List GetSpecializations() => + [ + new() { Id = 1, Name = "Терапевт", Code = "THERAPIST", Description = "Врач общей практики" }, + new() { Id = 2, Name = "Хирург", Code = "SURGEON", Description = "Проведение операций" }, + new() { Id = 3, Name = "Кардиолог", Code = "CARDIOLOGIST", Description = "Заболевания сердца" }, + new() { Id = 4, Name = "Невролог", Code = "NEUROLOGIST", Description = "Заболевания нервной системы" }, + new() { Id = 5, Name = "Педиатр", Code = "PEDIATRICIAN", Description = "Детские болезни" }, + new() { Id = 6, Name = "Гинеколог", Code = "GYNECOLOGIST", Description = "Женское здоровье" }, + new() { Id = 7, Name = "Офтальмолог", Code = "OPHTHALMOLOGIST", Description = "Заболевания глаз" }, + new() { Id = 8, Name = "Отоларинголог", Code = "ENT", Description = "Ухо, горло, нос" }, + new() { Id = 9, Name = "Дерматолог", Code = "DERMATOLOGIST", Description = "Кожные заболевания" }, + new() { Id = 10, Name = "Эндокринолог", Code = "ENDOCRINOLOGIST", Description = "Гормональные нарушения" } + ]; + + private List GetDoctors() => + [ + new() + { + Id = 1, + PassportNumber = "4501 123456", + FullName = "Иванов Иван Иванович", + BirthYear = 1980, + SpecializationId = 1, + ExperienceYears = 15 + }, + new() + { + Id = 2, + PassportNumber = "4502 234567", + FullName = "Петров Петр Петрович", + BirthYear = 1975, + SpecializationId = 2, + ExperienceYears = 20 + }, + new() + { + Id = 3, + PassportNumber = "4503 345678", + FullName = "Сидорова Анна Сергеевна", + BirthYear = 1985, + SpecializationId = 3, + ExperienceYears = 12 + }, + new() + { + Id = 4, + PassportNumber = "4504 456789", + FullName = "Козлов Дмитрий Николаевич", + BirthYear = 1990, + SpecializationId = 4, + ExperienceYears = 8 + }, + new() + { + Id = 5, + PassportNumber = "4505 567890", + FullName = "Морозова Елена Владимировна", + BirthYear = 1982, + SpecializationId = 5, + ExperienceYears = 14 + }, + new() + { + Id = 6, + PassportNumber = "4506 678901", + FullName = "Волков Андрей Игоревич", + BirthYear = 1978, + SpecializationId = 6, + ExperienceYears = 18 + }, + new() + { + Id = 7, + PassportNumber = "4507 789012", + FullName = "Соколова Татьяна Александровна", + BirthYear = 1988, + SpecializationId = 7, + ExperienceYears = 10 + }, + new() + { + Id = 8, + PassportNumber = "4508 890123", + FullName = "Лебедев Михаил Сергеевич", + BirthYear = 1992, + SpecializationId = 8, + ExperienceYears = 6 + }, + new() + { + Id = 9, + PassportNumber = "4509 901234", + FullName = "Николаева Ольга Викторовна", + BirthYear = 1983, + SpecializationId = 9, + ExperienceYears = 13 + }, + new() + { + Id = 10, + PassportNumber = "4510 012345", + FullName = "Федоров Алексей Павлович", + BirthYear = 1970, + SpecializationId = 10, + ExperienceYears = 25 + } + ]; + + private List GetPatients() => + [ + new() + { + Id = 1, + PassportNumber = "6001 123456", + FullName = "Смирнов Алексей Викторович", + Gender = Gender.Male, + BirthDate = new DateTime(1990, 5, 15), + Address = "ул. Ленина, д. 10, кв. 25", + BloodGroup = BloodGroup.A, + RhFactor = RhFactor.Positive, + PhoneNumber = "+7 (999) 123-45-67" + }, + new() + { + Id = 2, + PassportNumber = "6002 234567", + FullName = "Кузнецова Елена Дмитриевна", + Gender = Gender.Female, + BirthDate = new DateTime(1985, 8, 22), + Address = "ул. Гагарина, д. 5, кв. 12", + BloodGroup = BloodGroup.O, + RhFactor = RhFactor.Positive, + PhoneNumber = "+7 (999) 234-56-78" + }, + new() + { + Id = 3, + PassportNumber = "6003 345678", + FullName = "Попов Сергей Иванович", + Gender = Gender.Male, + BirthDate = new DateTime(1978, 3, 10), + Address = "пр. Мира, д. 15, кв. 7", + BloodGroup = BloodGroup.B, + RhFactor = RhFactor.Negative, + PhoneNumber = "+7 (999) 345-67-89" + }, + new() + { + Id = 4, + PassportNumber = "6004 456789", + FullName = "Васильева Мария Петровна", + Gender = Gender.Female, + BirthDate = new DateTime(1995, 11, 30), + Address = "ул. Советская, д. 8, кв. 42", + BloodGroup = BloodGroup.AB, + RhFactor = RhFactor.Positive, + PhoneNumber = "+7 (999) 456-78-90" + }, + new() + { + Id = 5, + PassportNumber = "6005 567890", + FullName = "Соколов Андрей Николаевич", + Gender = Gender.Male, + BirthDate = new DateTime(1982, 7, 18), + Address = "ул. Пушкина, д. 3, кв. 56", + BloodGroup = BloodGroup.A, + RhFactor = RhFactor.Negative, + PhoneNumber = "+7 (999) 567-89-01" + }, + new() + { + Id = 6, + PassportNumber = "6006 678901", + FullName = "Михайлова Анна Сергеевна", + Gender = Gender.Female, + BirthDate = new DateTime(1975, 9, 25), + Address = "пр. Ленинградский, д. 22, кв. 15", + BloodGroup = BloodGroup.O, + RhFactor = RhFactor.Positive, + PhoneNumber = "+7 (999) 678-90-12" + }, + new() + { + Id = 7, + PassportNumber = "6007 789012", + FullName = "Новиков Денис Александрович", + Gender = Gender.Male, + BirthDate = new DateTime(1988, 2, 14), + Address = "ул. Кирова, д. 12, кв. 8", + BloodGroup = BloodGroup.B, + RhFactor = RhFactor.Positive, + PhoneNumber = "+7 (999) 789-01-23" + }, + new() + { + Id = 8, + PassportNumber = "6008 890123", + FullName = "Морозова Татьяна Владимировна", + Gender = Gender.Female, + BirthDate = new DateTime(1992, 6, 5), + Address = "ул. Садовая, д. 7, кв. 31", + BloodGroup = BloodGroup.AB, + RhFactor = RhFactor.Negative, + PhoneNumber = "+7 (999) 890-12-34" + }, + new() + { + Id = 9, + PassportNumber = "6009 901234", + FullName = "Зайцев Игорь Павлович", + Gender = Gender.Male, + BirthDate = new DateTime(1970, 12, 3), + Address = "пр. Невский, д. 45, кв. 19", + BloodGroup = BloodGroup.A, + RhFactor = RhFactor.Positive, + PhoneNumber = "+7 (999) 901-23-45" + }, + new() + { + Id = 10, + PassportNumber = "6010 012345", + FullName = "Волкова Ольга Игоревна", + Gender = Gender.Female, + BirthDate = new DateTime(1965, 4, 20), + Address = "ул. Комсомольская, д. 6, кв. 23", + BloodGroup = BloodGroup.O, + RhFactor = RhFactor.Positive, + PhoneNumber = "+7 (999) 012-34-56" + }, + new() + { + Id = 11, + PassportNumber = "6011 123456", + FullName = "Белова Наталья Сергеевна", + Gender = Gender.Female, + BirthDate = new DateTime(1998, 1, 8), + Address = "ул. Мичурина, д. 18, кв. 67", + BloodGroup = BloodGroup.B, + RhFactor = RhFactor.Positive, + PhoneNumber = "+7 (999) 123-56-78" + }, + new() + { + Id = 12, + PassportNumber = "6012 234567", + FullName = "Карпов Евгений Владимирович", + Gender = Gender.Male, + BirthDate = new DateTime(1983, 9, 12), + Address = "ул. Лермонтова, д. 9, кв. 14", + BloodGroup = BloodGroup.AB, + RhFactor = RhFactor.Negative, + PhoneNumber = "+7 (999) 234-67-89" + } + ]; + + private List GetAppointments() + { + var appointments = new List(); + var appointmentId = 1; + + // Записи на текущий месяц (февраль 2026) + appointments.AddRange([ + new() + { + Id = appointmentId++, + AppointmentDateTime = new DateTime(2026, 2, 5, 10, 0, 0), + RoomNumber = TestConstants.TherapyRoom, + IsRepeat = false, + PatientId = 1, + DoctorId = 1 + }, + new() + { + Id = appointmentId++, + AppointmentDateTime = new DateTime(2026, 2, 5, 11, 0, 0), + RoomNumber = TestConstants.TherapyRoom, + IsRepeat = true, + PatientId = 2, + DoctorId = 1 + }, + new() + { + Id = appointmentId++, + AppointmentDateTime = new DateTime(2026, 2, 10, 14, 30, 0), + RoomNumber = TestConstants.SurgeryRoom, + IsRepeat = false, + PatientId = 3, + DoctorId = 2 + }, + new() + { + Id = appointmentId++, + AppointmentDateTime = new DateTime(2026, 2, 15, 9, 15, 0), + RoomNumber = TestConstants.CardiologyRoom, + IsRepeat = false, + PatientId = 4, + DoctorId = 3 + }, + new() + { + Id = appointmentId++, + AppointmentDateTime = new DateTime(2026, 2, 12, 13, 30, 0), + RoomNumber = TestConstants.TherapyRoom, + IsRepeat = false, + PatientId = 8, + DoctorId = 1 + }, + new() + { + Id = appointmentId++, + AppointmentDateTime = new DateTime(2026, 2, 16, 9, 30, 0), + RoomNumber = TestConstants.TherapyRoom, + IsRepeat = false, + PatientId = 11, + DoctorId = 1 + } + ]); + + // Записи на прошлый месяц (январь 2026) + appointments.AddRange([ + new() + { + Id = appointmentId++, + AppointmentDateTime = new DateTime(2026, 1, 15, 10, 0, 0), + RoomNumber = TestConstants.TherapyRoom, + IsRepeat = true, + PatientId = 6, + DoctorId = 2 + }, + new() + { + Id = appointmentId++, + AppointmentDateTime = new DateTime(2026, 1, 20, 11, 0, 0), + RoomNumber = TestConstants.SurgeryRoom, + IsRepeat = true, + PatientId = 7, + DoctorId = 2 + }, + new() + { + Id = appointmentId++, + AppointmentDateTime = new DateTime(2026, 1, 5, 9, 0, 0), + RoomNumber = TestConstants.CardiologyRoom, + IsRepeat = false, + PatientId = 8, + DoctorId = 3 + } + ]); + + // Пациент Соколов (Id=5) записан к нескольким врачам + appointments.AddRange([ + new() + { + Id = appointmentId++, + AppointmentDateTime = new DateTime(2026, 2, 8, 9, 0, 0), + RoomNumber = "102", + IsRepeat = false, + PatientId = 5, + DoctorId = 2 + }, + new() + { + Id = appointmentId++, + AppointmentDateTime = new DateTime(2026, 2, 22, 11, 30, 0), + RoomNumber = "606", + IsRepeat = false, + PatientId = 5, + DoctorId = 6 + } + ]); + + return appointments; + } + + private void LinkDoctorsWithSpecializations() + { + foreach (var doctor in Doctors) + { + doctor.Specialization = Specializations.First(s => s.Id == doctor.SpecializationId); + doctor.Specialization.Doctors.Add(doctor); + } + } + + private void LinkAppointmentsWithDoctorsAndPatients() + { + foreach (var appointment in Appointments) + { + appointment.Doctor = Doctors.First(d => d.Id == appointment.DoctorId); + appointment.Patient = Patients.First(p => p.Id == appointment.PatientId); + + appointment.Doctor.Appointments.Add(appointment); + appointment.Patient.Appointments.Add(appointment); + } + } +} \ No newline at end of file diff --git a/Polyclinic/Polyclinic.Tests/PolyclinicTests.cs b/Polyclinic/Polyclinic.Tests/PolyclinicTests.cs new file mode 100644 index 000000000..4eee79fa9 --- /dev/null +++ b/Polyclinic/Polyclinic.Tests/PolyclinicTests.cs @@ -0,0 +1,175 @@ +using Polyclinic.Domain.Entities; + +namespace Polyclinic.Tests; + +/// +/// Тесты для поликлиники с использованием фикстуры +/// +public class PolyclinicTests(PolyclinicFixture fixture) : IClassFixture +{ + /// + /// ТЕСТ 1: Вывести информацию о всех врачах, стаж работы которых не менее 10 лет + /// + [Fact] + public void GetDoctorsWithExperienceMoreThan10Years() + { + var actual = ( + from doctor in fixture.Doctors + where doctor.ExperienceYears >= TestConstants.MinExperienceYears + orderby doctor.FullName + select doctor + ).ToList(); + + Assert.NotEmpty(actual); + Assert.All(actual, doctor => + Assert.True(doctor.ExperienceYears >= TestConstants.MinExperienceYears)); + + var excludedDoctors = fixture.Doctors.Except(actual); + Assert.All(excludedDoctors, doctor => + Assert.True(doctor.ExperienceYears < TestConstants.MinExperienceYears)); + + Assert.Equal([.. actual.OrderBy(d => d.FullName)], actual); + } + + /// + /// ТЕСТ 2: Вывести информацию о всех пациентах, записанных к указанному врачу, упорядочить по ФИО + /// + [Fact] + public void GetPatientsByDoctorOrderedByFullName() + { + var testDoctor = fixture.Doctors.First(d => fixture.Appointments.Any(a => a.DoctorId == d.Id)); + + var actual = ( + from appointment in fixture.Appointments + where appointment.DoctorId == testDoctor.Id + join patient in fixture.Patients on appointment.PatientId equals patient.Id + orderby patient.FullName + select patient + ).Distinct().ToList(); + + Assert.NotEmpty(actual); + Assert.All(actual, patient => + { + var hasAppointmentWithDoctor = fixture.Appointments + .Any(a => a.PatientId == patient.Id && a.DoctorId == testDoctor.Id); + Assert.True(hasAppointmentWithDoctor); + }); + + Assert.Equal([.. actual.OrderBy(p => p.FullName)], actual); + } + + /// + /// ТЕСТ 3: Вывести информацию о количестве повторных приемов пациентов за последний месяц + /// + [Fact] + public void CountRepeatAppointmentsLastMonth() + { + var actual = ( + from appointment in fixture.Appointments + where appointment.AppointmentDateTime >= TestConstants.StartOfLastMonth + where appointment.AppointmentDateTime < TestConstants.StartOfCurrentMonth + where appointment.IsRepeat + select appointment + ).ToList(); + + Assert.All(actual, appointment => + { + Assert.True(appointment.IsRepeat); + Assert.True(appointment.AppointmentDateTime >= TestConstants.StartOfLastMonth); + Assert.True(appointment.AppointmentDateTime < TestConstants.StartOfCurrentMonth); + }); + + var nonRepeatInLastMonth = fixture.Appointments + .Where(a => a.AppointmentDateTime >= TestConstants.StartOfLastMonth) + .Where(a => a.AppointmentDateTime < TestConstants.StartOfCurrentMonth) + .Where(a => !a.IsRepeat); + + Assert.All(nonRepeatInLastMonth, a => Assert.False(a.IsRepeat)); + } + + /// + /// ТЕСТ 4: Вывести информацию о пациентах старше 30 лет, + /// которые записаны на прием к нескольким врачам, упорядочить по дате рождения + /// + [Fact] + public void GetPatientsOver30WithMultipleDoctorsOrderedByBirthDate() + { + var actual = ( + from patient in fixture.Patients + where patient.GetAge(TestConstants.Today) >= TestConstants.MinPatientAge + let doctorsCount = ( + from appointment in fixture.Appointments + where appointment.PatientId == patient.Id + select appointment.DoctorId + ).Distinct().Count() + where doctorsCount >= 2 + orderby patient.BirthDate + select patient + ).ToList(); + + Assert.All(actual, patient => + { + Assert.True(patient.GetAge(TestConstants.Today) >= TestConstants.MinPatientAge); + + var doctorsCount = fixture.Appointments + .Where(a => a.PatientId == patient.Id) + .Select(a => a.DoctorId) + .Distinct() + .Count(); + Assert.True(doctorsCount >= 2); + }); + + var excludedPatients = fixture.Patients.Except(actual); + Assert.All(excludedPatients, patient => + { + if (patient.GetAge(TestConstants.Today) >= TestConstants.MinPatientAge) + { + var doctorsCount = fixture.Appointments + .Where(a => a.PatientId == patient.Id) + .Select(a => a.DoctorId) + .Distinct() + .Count(); + Assert.True(doctorsCount < 2); + } + }); + + Assert.Equal([.. actual.OrderBy(p => p.BirthDate)], actual); + } + + /// + /// ТЕСТ 5: Вывести информацию о приемах за текущий месяц, проходящих в выбранном кабинете + /// + [Fact] + public void GetAppointmentsCurrentMonthInRoom() + { + var testRoom = fixture.Appointments + .First(a => a.AppointmentDateTime >= TestConstants.StartOfCurrentMonth && + a.AppointmentDateTime < TestConstants.StartOfCurrentMonth.AddMonths(1)) + .RoomNumber; + + var actual = ( + from appointment in fixture.Appointments + where appointment.RoomNumber == testRoom + where appointment.AppointmentDateTime >= TestConstants.StartOfCurrentMonth + where appointment.AppointmentDateTime < TestConstants.StartOfCurrentMonth.AddMonths(1) + orderby appointment.AppointmentDateTime + select appointment + ).ToList(); + + Assert.NotEmpty(actual); + Assert.All(actual, appointment => + { + Assert.Equal(testRoom, appointment.RoomNumber); + Assert.True(appointment.AppointmentDateTime >= TestConstants.StartOfCurrentMonth); + Assert.True(appointment.AppointmentDateTime < TestConstants.StartOfCurrentMonth.AddMonths(1)); + }); + + var otherRoomAppointments = fixture.Appointments + .Where(a => a.AppointmentDateTime >= TestConstants.StartOfCurrentMonth) + .Where(a => a.AppointmentDateTime < TestConstants.StartOfCurrentMonth.AddMonths(1)) + .Where(a => a.RoomNumber != testRoom); + + Assert.All(otherRoomAppointments, a => Assert.NotEqual(testRoom, a.RoomNumber)); + Assert.Equal([.. actual.OrderBy(a => a.AppointmentDateTime)], actual); + } +} \ No newline at end of file diff --git a/Polyclinic/Polyclinic.Tests/TestConstants.cs b/Polyclinic/Polyclinic.Tests/TestConstants.cs new file mode 100644 index 000000000..53541a3f7 --- /dev/null +++ b/Polyclinic/Polyclinic.Tests/TestConstants.cs @@ -0,0 +1,47 @@ +namespace Polyclinic.Tests; + +/// +/// Константы для тестов +/// +public static class TestConstants +{ + /// + /// Фиксированная дата для тестов (15 февраля 2026) + /// + public static readonly DateTime Today = new(2026, 2, 15); + + /// + /// Начало текущего месяца + /// + public static readonly DateTime StartOfCurrentMonth = new(Today.Year, Today.Month, 1); + + /// + /// Начало прошлого месяца + /// + public static readonly DateTime StartOfLastMonth = StartOfCurrentMonth.AddMonths(-1); + + /// + /// Минимальный стаж для опытных врачей + /// + public const int MinExperienceYears = 10; + + /// + /// Минимальный возраст для "пациенты старше 30 лет" + /// + public const int MinPatientAge = 30; + + /// + /// Номер кабинета терапевта + /// + public const string TherapyRoom = "101"; + + /// + /// Номер кабинета хирурга + /// + public const string SurgeryRoom = "202"; + + /// + /// Номер кабинета кардиолога + /// + public const string CardiologyRoom = "303"; +} \ No newline at end of file diff --git a/Polyclinic/Polyclinic.sln b/Polyclinic/Polyclinic.sln new file mode 100644 index 000000000..28573c5ce --- /dev/null +++ b/Polyclinic/Polyclinic.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36915.13 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Polyclinic.Domain", "Polyclinic.Domain\Polyclinic.Domain.csproj", "{6C9C08E8-CE8E-422F-8BDC-E7B751E8E84A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Polyclinic.Tests", "Polyclinic.Tests\Polyclinic.Tests.csproj", "{0C24BBBB-C432-4776-9968-5DAD9432AB5A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6C9C08E8-CE8E-422F-8BDC-E7B751E8E84A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6C9C08E8-CE8E-422F-8BDC-E7B751E8E84A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6C9C08E8-CE8E-422F-8BDC-E7B751E8E84A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6C9C08E8-CE8E-422F-8BDC-E7B751E8E84A}.Release|Any CPU.Build.0 = Release|Any CPU + {0C24BBBB-C432-4776-9968-5DAD9432AB5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0C24BBBB-C432-4776-9968-5DAD9432AB5A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0C24BBBB-C432-4776-9968-5DAD9432AB5A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0C24BBBB-C432-4776-9968-5DAD9432AB5A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {1F0773E0-B943-4E7B-A2FB-1B30E6ADA139} + EndGlobalSection +EndGlobal diff --git a/README.md b/README.md index 76afcbfdd..5b1298a8d 100644 --- a/README.md +++ b/README.md @@ -1,137 +1,51 @@ -# Разработка корпоративных приложений -[Таблица с успеваемостью](https://docs.google.com/spreadsheets/d/1JD6aiOG6r7GrA79oJncjgUHWtfeW4g_YZ9ayNgxb_w0/edit?usp=sharing) +# Разработка корпоративных приложений. Лабораторная работа №1. -## Задание -### Цель -Реализация проекта сервисно-ориентированного приложения. +### Цель - Реализация объектной модели данных и unit-тестов -### Задачи -* Реализация объектно-ориентированной модели данных, -* Изучение реализации серверных приложений на базе WebAPI/OpenAPI, -* Изучение работы с брокерами сообщений, -* Изучение паттернов проектирования, -* Изучение работы со средствами оркестрации на примере .NET Aspire, -* Повторение основ работы с системами контроля версий, -* Unit-тестирование. +Необходимо подготовить структуру классов, описывающих предметную область. В каждом из заданий присутствует часть, связанная с обработкой данных, представленная в разделе «Unit-тесты». Данную часть необходимо реализовать в виде unit-тестов: подготовить тестовые данные, выполнить запрос с использованием LINQ, проверить результаты. Хранение данных на этом этапе допускается осуществлять в памяти в виде коллекций. -### Лабораторные работы -
-1. «Классы» - Реализация объектной модели данных и unit-тестов -
-В рамках первой лабораторной работы необходимо подготовить структуру классов, описывающих предметную область, определяемую в задании. В каждом из заданий присутствует часть, связанная с обработкой данных, представленная в разделе «Unit-тесты». Данную часть необходимо реализовать в виде unit-тестов: подготовить тестовые данные, выполнить запрос с использованием LINQ, проверить результаты. +### Предметная область - Поликлиника -Хранение данных на этом этапе допускается осуществлять в памяти в виде коллекций. -Необходимо включить **как минимум 10** экземпляров каждого класса в датасид. - -
-
-2. «Сервер» - Реализация серверного приложения с использованием REST API -
-Во второй лабораторной работе необходимо реализовать серверное приложение, которое должно: -- Осуществлять базовые CRUD-операции с реализованными в первой лабораторной сущностями -- Предоставлять результаты аналитических запросов (раздел «Unit-тесты» задания) +В базе данных поликлиники содержится информация о записях пациентов на прием к врачам. -Хранение данных на этом этапе допускается осуществлять в памяти в виде коллекций. -
-
-
-3. «ORM» - Реализация объектно-реляционной модели. Подключение к базе данных и настройка оркестрации -
-В третьей лабораторной работе хранение должно быть переделано c инмемори коллекций на базу данных. -Должны быть созданы миграции для создания таблиц в бд и их первоначального заполнения. -
-Также необходимо настроить оркестратор Aspire на запуск сервера и базы данных. -
-
-
-4. «Инфраструктура» - Реализация сервиса генерации данных и его интеграция с сервером -
-В четвертой лабораторной работе необходимо имплементировать сервис, который генерировал бы контракты. Контракты далее передаются в сервер и сохраняются в бд. -Сервис должен представлять из себя отдельное приложение без референсов к серверным проектам за исключением библиотеки с контрактами. -Отправка контрактов при помощи gRPC должна выполняться в потоковом виде. -При использовании брокеров сообщений, необходимо предусмотреть ретраи при подключении к брокеру. +Пациент характеризуется: номером паспорта, ФИО, полом, датой рождения, адресом, группой крови, резус фактором и контактным телефоном. +Пол пациента является перечислением. +Группа крови пациента является перечислением. +Резус фактор пациента является перечислением. -Также необходимо добавить в конфигурацию Aspire запуск генератора и (если того требует вариант) брокера сообщений. -
-
-
-5. «Клиент» - Интеграция клиентского приложения с оркестратором -
-В пятой лабораторной необходимо добавить в конфигурацию Aspire запуск клиентского приложения для написанного ранее сервера. Клиент создается в рамках курса "Веб разработка". -
-
+Информация о враче включает номер паспорта, ФИО, год рождения, специализацию, стаж работы. Специализация врача является справочником. -## Задание. Общая часть -**Обязательно**: -* Реализация серверной части на [.NET 8](https://learn.microsoft.com/ru-ru/dotnet/core/whats-new/dotnet-8/overview). -* Реализация серверной части на [ASP.NET](https://dotnet.microsoft.com/ru-ru/apps/aspnet). -* Реализация unit-тестов с использованием [xUnit](https://xunit.net/?tabs=cs). -* Использование хранения данных в базе данных согласно варианту задания. -* Оркестрация проектов при помощи [.NET Aspire](https://learn.microsoft.com/ru-ru/dotnet/aspire/get-started/aspire-overview) -* Реализация сервиса генерации данных при помощи [Bogus](https://github.com/bchavez/Bogus) и его взаимодейсвие с сервером согласно варианту задания. -* Автоматизация тестирования на уровне репозитория через [GitHub Actions](https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions). -* Создание минимальной документации к проекту: страница на GitHub с информацией о задании, скриншоты приложения и прочая информация. +При записи на прием пациента в базе данных фиксируется дата и время приема, номер кабинета, а также индикатор того, является ли прием повторным. +Используется в качестве контракта. -**Факультативно**: -* Реализация авторизации/аутентификации. -* Реализация atomic batch publishing/atomic batch consumption для брокеров, поддерживающих такой функционал. -* Реализация интеграционных тестов при помощи .NET Aspire. -* Реализация клиента на Blazor WASM. +### Юнит-тесты -Внимательно прочитайте [дискуссии](https://github.com/itsecd/enterprise-development/discussions/1) о том, как работает автоматическое распределение на ревью. -Сразу корректно называйте свои pr, чтобы они попали на ревью нужному преподавателю. + - Вывести информацию о всех врачах, стаж работы которых не менее 10 лет. + - Вывести информацию о всех пациентах, записанных на прием к указанному врачу, упорядочить по ФИО. + - Вывести информацию о количестве повторных приемов пациентов за последний месяц. + - Вывести информацию о пациентах старше 30 лет, которые записаны на прием к нескольким врачам, упорядочить по дате рождения. + - Вывести информацию о приемах за текущий месяц, проходящих в выбранном кабинете. -По итогу работы в семестре должна получиться следующая информационная система: -
-C4 диаграмма +### Структура проекта -image1 +Polyclinic (Solution) +│ +├── Polyclinic.Domain (Class Library) +│ ├── Entities/ +│ │ ├── Appointment.cs +│ │ ├── Doctor.cs +│ │ ├── Patient.cs +│ │ └── Specialization.cs +│ │ +│ └── Enums/ +│ ├── BloodGroup.cs +│ ├── Gender.cs +│ └── RhFactor.cs +│ +└── Polyclinic.Tests (xUnit) + ├── PolyclinicFixture.cs + ├── PolyclinicTests.cs + └── TestConstants.cs -
-## Варианты заданий -Номер варианта задания присваивается в начале семестра. Изменить его нельзя. Каждый вариант имеет уникальную комбинацию из предметной области, базы данных и технологии для общения сервиса генерации данных и сервера апи. -[Список вариантов](https://docs.google.com/document/d/1Wc8AvsKS_1JptpsxHO-cwfAxz2ghxvQRQ0fy4el2ZOc/edit?usp=sharing) -[Список предметных областей](https://docs.google.com/document/d/15jWhXMwd2K8giFMKku_yrY_s2uQNEu4ugJXLYPvYJAE/edit?usp=sharing) -[Вопросы к экзамену](https://docs.google.com/document/d/1bjfvtzjyMljJbcu8YCvC8DzDegDUAmDeNtBz9M6FQes/edit?usp=sharing) - -## Схема сдачи - -На каждую из лабораторных работ необходимо сделать отдельный [Pull Request (PR)](https://docs.github.com/en/pull-requests). - -Общая схема: -1. Сделать форк данного репозитория -2. Выполнить задание -3. Сделать PR в данный репозиторий -4. Исправить замечания после code review -5. Получить approve -6. Прийти на занятие и защитить работу - -## Критерии оценивания - -Конкурентный принцип. -Так как задания в первой лабораторной будут повторяться между студентами, то выделяются следующие показатели для оценки: -1. Скорость разработки -2. Качество разработки -3. Полнота выполнения задания - -Быстрее делаете PR - у вас преимущество. -Быстрее получаете Approve - у вас преимущество. -Выполните нечто немного выходящее за рамки проекта - у вас преимущество. - -### Шкала оценивания - -- **3 балла** за качество кода, из них: - - 2 балла - базовая оценка - - 1 балл (но не более) можно получить за выполнение любого из следующих пунктов: - - Реализация факультативного функционала - - Выполнение работы раньше других: первые 5 человек из каждой группы, которые сделали PR и получили approve, получают дополнительный балл -- **3 балла** за защиту: при сдаче лабораторной работы вам задается 3 вопроса, за каждый правильный ответ - 1 балл - -У вас 2 попытки пройти ревью (первичное ревью, ревью по результатам исправления). Если замечания по итогу не исправлены, то снимается один балл за код лабораторной работы. - -## Вопросы и обратная связь по курсу - -Чтобы задать вопрос по лабораторной, воспользуйтесь [соотвествующим разделом дискуссий](https://github.com/itsecd/enterprise-development/discussions/categories/questions) или заведите [ишью](https://github.com/itsecd/enterprise-development/issues/new). -Если у вас появились идеи/пожелания/прочие полезные мысли по преподаваемой дисциплине, их можно оставить [здесь](https://github.com/itsecd/enterprise-development/discussions/categories/ideas). From c4503953703aabd931aa3367eb1ce2d2f93dfac0 Mon Sep 17 00:00:00 2001 From: Tattaboe Date: Fri, 13 Feb 2026 11:37:24 +0400 Subject: [PATCH 2/7] Fix github actions workflow --- .github/workflows/dotnet_tests.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/dotnet_tests.yml diff --git a/.github/workflows/dotnet_tests.yml b/.github/workflows/dotnet_tests.yml new file mode 100644 index 000000000..ac58b9bb8 --- /dev/null +++ b/.github/workflows/dotnet_tests.yml @@ -0,0 +1,28 @@ +name: .NET Tests + +on: + push: + branches: [ "main", "master" ] + pull_request: + branches: [ "main", "master" ] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Restore dependencies + run: dotnet restore Polyclinic.sln + + - name: Build + run: dotnet build Polyclinic.sln --no-restore --configuration Release + + - name: Test + run: dotnet test Polyclinic.sln --no-build --configuration Release --verbosity normal \ No newline at end of file From ead176fdda733f4279de645f7d80a500bbb9b347 Mon Sep 17 00:00:00 2001 From: Tattaboe Date: Fri, 13 Feb 2026 11:42:36 +0400 Subject: [PATCH 3/7] Fix github actions workflow. YML file --- .github/workflows/dotnet_tests.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dotnet_tests.yml b/.github/workflows/dotnet_tests.yml index ac58b9bb8..1f55f21a3 100644 --- a/.github/workflows/dotnet_tests.yml +++ b/.github/workflows/dotnet_tests.yml @@ -19,10 +19,10 @@ jobs: dotnet-version: 8.0.x - name: Restore dependencies - run: dotnet restore Polyclinic.sln + run: dotnet restore Polyclinic/Polyclinic.sln - name: Build - run: dotnet build Polyclinic.sln --no-restore --configuration Release + run: dotnet build Polyclinic/Polyclinic.sln --no-restore --configuration Release - name: Test - run: dotnet test Polyclinic.sln --no-build --configuration Release --verbosity normal \ No newline at end of file + run: dotnet test Polyclinic/Polyclinic.sln --no-build --configuration Release --verbosity normal \ No newline at end of file From f2c7b75a7c2f88b23b26f2ada17417e8350fc3e1 Mon Sep 17 00:00:00 2001 From: Tattaboe Date: Sat, 14 Feb 2026 01:41:02 +0400 Subject: [PATCH 4/7] Fix: static method, Doctor BirthDate, BloodGroup Ab and more --- .../Polyclinic.Domain/Entities/Appointment.cs | 8 +-- .../Polyclinic.Domain/Entities/Doctor.cs | 21 +++--- .../Polyclinic.Domain/Entities/Patient.cs | 8 +-- .../Entities/Specialization.cs | 9 +-- .../Polyclinic.Domain/Enums/BloodGroup.cs | 10 +-- Polyclinic/Polyclinic.Domain/Enums/Gender.cs | 20 ++---- .../Polyclinic.Domain/Enums/RhFactor.cs | 8 +-- .../Polyclinic.Tests/PolyclinicFixture.cs | 70 +++++++------------ .../Polyclinic.Tests/PolyclinicTests.cs | 4 +- 9 files changed, 49 insertions(+), 109 deletions(-) diff --git a/Polyclinic/Polyclinic.Domain/Entities/Appointment.cs b/Polyclinic/Polyclinic.Domain/Entities/Appointment.cs index b9031c89c..627d57291 100644 --- a/Polyclinic/Polyclinic.Domain/Entities/Appointment.cs +++ b/Polyclinic/Polyclinic.Domain/Entities/Appointment.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Polyclinic.Domain.Entities; +namespace Polyclinic.Domain.Entities; /// /// Запись пациента на прием к врачу diff --git a/Polyclinic/Polyclinic.Domain/Entities/Doctor.cs b/Polyclinic/Polyclinic.Domain/Entities/Doctor.cs index 907e42a27..94c610777 100644 --- a/Polyclinic/Polyclinic.Domain/Entities/Doctor.cs +++ b/Polyclinic/Polyclinic.Domain/Entities/Doctor.cs @@ -1,12 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -using Polyclinic.Domain.Enums; - -namespace Polyclinic.Domain.Entities; +namespace Polyclinic.Domain.Entities; /// /// Врач поликлиники @@ -29,9 +21,9 @@ public class Doctor public required string FullName { get; set; } /// - /// Год рождения + /// Дата рождения /// - public int BirthYear { get; set; } + public DateTime BirthDate { get; set; } /// /// Идентификатор специализации @@ -56,5 +48,10 @@ public class Doctor /// /// Вычисление возраста врача на указанную дату /// - public int GetAge(DateTime onDate) => onDate.Year - BirthYear; + public int GetAge(DateTime onDate) + { + var age = onDate.Year - BirthDate.Year; + if (BirthDate.Date > onDate.AddYears(-age)) age--; + return age; + } } \ No newline at end of file diff --git a/Polyclinic/Polyclinic.Domain/Entities/Patient.cs b/Polyclinic/Polyclinic.Domain/Entities/Patient.cs index cd40dc67c..aab8ee069 100644 --- a/Polyclinic/Polyclinic.Domain/Entities/Patient.cs +++ b/Polyclinic/Polyclinic.Domain/Entities/Patient.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -using Polyclinic.Domain.Enums; +using Polyclinic.Domain.Enums; namespace Polyclinic.Domain.Entities; diff --git a/Polyclinic/Polyclinic.Domain/Entities/Specialization.cs b/Polyclinic/Polyclinic.Domain/Entities/Specialization.cs index 9aadf9fda..6f728be3f 100644 --- a/Polyclinic/Polyclinic.Domain/Entities/Specialization.cs +++ b/Polyclinic/Polyclinic.Domain/Entities/Specialization.cs @@ -1,11 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using System.Text; -using System.Threading.Tasks; - -namespace Polyclinic.Domain.Entities; +namespace Polyclinic.Domain.Entities; /// /// Специализация врача (справочник) diff --git a/Polyclinic/Polyclinic.Domain/Enums/BloodGroup.cs b/Polyclinic/Polyclinic.Domain/Enums/BloodGroup.cs index 10d251989..f9a6f0722 100644 --- a/Polyclinic/Polyclinic.Domain/Enums/BloodGroup.cs +++ b/Polyclinic/Polyclinic.Domain/Enums/BloodGroup.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Polyclinic.Domain.Enums; +namespace Polyclinic.Domain.Enums; /// /// Группа крови пациента @@ -29,5 +23,5 @@ public enum BloodGroup /// /// Четвертая (AB) /// - AB + Ab } diff --git a/Polyclinic/Polyclinic.Domain/Enums/Gender.cs b/Polyclinic/Polyclinic.Domain/Enums/Gender.cs index 09d9260a7..2f909a5c4 100644 --- a/Polyclinic/Polyclinic.Domain/Enums/Gender.cs +++ b/Polyclinic/Polyclinic.Domain/Enums/Gender.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Polyclinic.Domain.Enums; +namespace Polyclinic.Domain.Enums; /// /// Пол пациента @@ -12,17 +6,17 @@ namespace Polyclinic.Domain.Enums; public enum Gender { /// - /// Мужской + /// Не указано /// - Male, + NotSet, /// - /// Женский + /// Мужской /// - Female, + Male, /// - /// Не указано + /// Женский /// - Other + Female } diff --git a/Polyclinic/Polyclinic.Domain/Enums/RhFactor.cs b/Polyclinic/Polyclinic.Domain/Enums/RhFactor.cs index ed4b1726e..0766c45e3 100644 --- a/Polyclinic/Polyclinic.Domain/Enums/RhFactor.cs +++ b/Polyclinic/Polyclinic.Domain/Enums/RhFactor.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Polyclinic.Domain.Enums; +namespace Polyclinic.Domain.Enums; /// /// Резус-фактор пациента diff --git a/Polyclinic/Polyclinic.Tests/PolyclinicFixture.cs b/Polyclinic/Polyclinic.Tests/PolyclinicFixture.cs index 54e65d018..0dd92b41b 100644 --- a/Polyclinic/Polyclinic.Tests/PolyclinicFixture.cs +++ b/Polyclinic/Polyclinic.Tests/PolyclinicFixture.cs @@ -8,29 +8,11 @@ namespace Polyclinic.Tests; /// public class PolyclinicFixture { - /// - /// Список специализаций - /// public List Specializations { get; } - - /// - /// Список врачей - /// public List Doctors { get; } - - /// - /// Список пациентов - /// public List Patients { get; } - - /// - /// Список записей на прием - /// public List Appointments { get; } - /// - /// Конструктор, инициализирующий тестовые данные - /// public PolyclinicFixture() { Specializations = GetSpecializations(); @@ -42,7 +24,7 @@ public PolyclinicFixture() LinkAppointmentsWithDoctorsAndPatients(); } - private List GetSpecializations() => + private static List GetSpecializations() => [ new() { Id = 1, Name = "Терапевт", Code = "THERAPIST", Description = "Врач общей практики" }, new() { Id = 2, Name = "Хирург", Code = "SURGEON", Description = "Проведение операций" }, @@ -56,14 +38,14 @@ private List GetSpecializations() => new() { Id = 10, Name = "Эндокринолог", Code = "ENDOCRINOLOGIST", Description = "Гормональные нарушения" } ]; - private List GetDoctors() => + private static List GetDoctors() => [ new() { Id = 1, PassportNumber = "4501 123456", FullName = "Иванов Иван Иванович", - BirthYear = 1980, + BirthDate = new DateTime(1980, 5, 15), SpecializationId = 1, ExperienceYears = 15 }, @@ -72,7 +54,7 @@ private List GetDoctors() => Id = 2, PassportNumber = "4502 234567", FullName = "Петров Петр Петрович", - BirthYear = 1975, + BirthDate = new DateTime(1975, 8, 22), SpecializationId = 2, ExperienceYears = 20 }, @@ -81,7 +63,7 @@ private List GetDoctors() => Id = 3, PassportNumber = "4503 345678", FullName = "Сидорова Анна Сергеевна", - BirthYear = 1985, + BirthDate = new DateTime(1985, 3, 10), SpecializationId = 3, ExperienceYears = 12 }, @@ -90,7 +72,7 @@ private List GetDoctors() => Id = 4, PassportNumber = "4504 456789", FullName = "Козлов Дмитрий Николаевич", - BirthYear = 1990, + BirthDate = new DateTime(1990, 11, 30), SpecializationId = 4, ExperienceYears = 8 }, @@ -99,7 +81,7 @@ private List GetDoctors() => Id = 5, PassportNumber = "4505 567890", FullName = "Морозова Елена Владимировна", - BirthYear = 1982, + BirthDate = new DateTime(1982, 7, 18), SpecializationId = 5, ExperienceYears = 14 }, @@ -108,7 +90,7 @@ private List GetDoctors() => Id = 6, PassportNumber = "4506 678901", FullName = "Волков Андрей Игоревич", - BirthYear = 1978, + BirthDate = new DateTime(1978, 9, 25), SpecializationId = 6, ExperienceYears = 18 }, @@ -117,7 +99,7 @@ private List GetDoctors() => Id = 7, PassportNumber = "4507 789012", FullName = "Соколова Татьяна Александровна", - BirthYear = 1988, + BirthDate = new DateTime(1988, 2, 14), SpecializationId = 7, ExperienceYears = 10 }, @@ -126,7 +108,7 @@ private List GetDoctors() => Id = 8, PassportNumber = "4508 890123", FullName = "Лебедев Михаил Сергеевич", - BirthYear = 1992, + BirthDate = new DateTime(1992, 6, 5), SpecializationId = 8, ExperienceYears = 6 }, @@ -135,7 +117,7 @@ private List GetDoctors() => Id = 9, PassportNumber = "4509 901234", FullName = "Николаева Ольга Викторовна", - BirthYear = 1983, + BirthDate = new DateTime(1983, 12, 3), SpecializationId = 9, ExperienceYears = 13 }, @@ -144,13 +126,13 @@ private List GetDoctors() => Id = 10, PassportNumber = "4510 012345", FullName = "Федоров Алексей Павлович", - BirthYear = 1970, + BirthDate = new DateTime(1970, 4, 20), SpecializationId = 10, ExperienceYears = 25 } ]; - private List GetPatients() => + private static List GetPatients() => [ new() { @@ -196,7 +178,7 @@ private List GetPatients() => Gender = Gender.Female, BirthDate = new DateTime(1995, 11, 30), Address = "ул. Советская, д. 8, кв. 42", - BloodGroup = BloodGroup.AB, + BloodGroup = BloodGroup.Ab, RhFactor = RhFactor.Positive, PhoneNumber = "+7 (999) 456-78-90" }, @@ -244,7 +226,7 @@ private List GetPatients() => Gender = Gender.Female, BirthDate = new DateTime(1992, 6, 5), Address = "ул. Садовая, д. 7, кв. 31", - BloodGroup = BloodGroup.AB, + BloodGroup = BloodGroup.Ab, RhFactor = RhFactor.Negative, PhoneNumber = "+7 (999) 890-12-34" }, @@ -292,13 +274,13 @@ private List GetPatients() => Gender = Gender.Male, BirthDate = new DateTime(1983, 9, 12), Address = "ул. Лермонтова, д. 9, кв. 14", - BloodGroup = BloodGroup.AB, + BloodGroup = BloodGroup.Ab, RhFactor = RhFactor.Negative, PhoneNumber = "+7 (999) 234-67-89" } ]; - private List GetAppointments() + private static List GetAppointments() { var appointments = new List(); var appointmentId = 1; @@ -309,7 +291,7 @@ private List GetAppointments() { Id = appointmentId++, AppointmentDateTime = new DateTime(2026, 2, 5, 10, 0, 0), - RoomNumber = TestConstants.TherapyRoom, + RoomNumber = "101", IsRepeat = false, PatientId = 1, DoctorId = 1 @@ -318,7 +300,7 @@ private List GetAppointments() { Id = appointmentId++, AppointmentDateTime = new DateTime(2026, 2, 5, 11, 0, 0), - RoomNumber = TestConstants.TherapyRoom, + RoomNumber = "101", IsRepeat = true, PatientId = 2, DoctorId = 1 @@ -327,7 +309,7 @@ private List GetAppointments() { Id = appointmentId++, AppointmentDateTime = new DateTime(2026, 2, 10, 14, 30, 0), - RoomNumber = TestConstants.SurgeryRoom, + RoomNumber = "202", IsRepeat = false, PatientId = 3, DoctorId = 2 @@ -336,7 +318,7 @@ private List GetAppointments() { Id = appointmentId++, AppointmentDateTime = new DateTime(2026, 2, 15, 9, 15, 0), - RoomNumber = TestConstants.CardiologyRoom, + RoomNumber = "303", IsRepeat = false, PatientId = 4, DoctorId = 3 @@ -345,7 +327,7 @@ private List GetAppointments() { Id = appointmentId++, AppointmentDateTime = new DateTime(2026, 2, 12, 13, 30, 0), - RoomNumber = TestConstants.TherapyRoom, + RoomNumber = "101", IsRepeat = false, PatientId = 8, DoctorId = 1 @@ -354,7 +336,7 @@ private List GetAppointments() { Id = appointmentId++, AppointmentDateTime = new DateTime(2026, 2, 16, 9, 30, 0), - RoomNumber = TestConstants.TherapyRoom, + RoomNumber = "101", IsRepeat = false, PatientId = 11, DoctorId = 1 @@ -367,7 +349,7 @@ private List GetAppointments() { Id = appointmentId++, AppointmentDateTime = new DateTime(2026, 1, 15, 10, 0, 0), - RoomNumber = TestConstants.TherapyRoom, + RoomNumber = "101", IsRepeat = true, PatientId = 6, DoctorId = 2 @@ -376,7 +358,7 @@ private List GetAppointments() { Id = appointmentId++, AppointmentDateTime = new DateTime(2026, 1, 20, 11, 0, 0), - RoomNumber = TestConstants.SurgeryRoom, + RoomNumber = "202", IsRepeat = true, PatientId = 7, DoctorId = 2 @@ -385,7 +367,7 @@ private List GetAppointments() { Id = appointmentId++, AppointmentDateTime = new DateTime(2026, 1, 5, 9, 0, 0), - RoomNumber = TestConstants.CardiologyRoom, + RoomNumber = "303", IsRepeat = false, PatientId = 8, DoctorId = 3 diff --git a/Polyclinic/Polyclinic.Tests/PolyclinicTests.cs b/Polyclinic/Polyclinic.Tests/PolyclinicTests.cs index 4eee79fa9..d9c3a412e 100644 --- a/Polyclinic/Polyclinic.Tests/PolyclinicTests.cs +++ b/Polyclinic/Polyclinic.Tests/PolyclinicTests.cs @@ -1,6 +1,4 @@ -using Polyclinic.Domain.Entities; - -namespace Polyclinic.Tests; +namespace Polyclinic.Tests; /// /// Тесты для поликлиники с использованием фикстуры From 6b31bf860bd810893a67b655772ad85da677cb44 Mon Sep 17 00:00:00 2001 From: Tattaboe Date: Mon, 16 Feb 2026 23:42:14 +0400 Subject: [PATCH 5/7] Lab2, Lab3: Complete --- .../Controllers/AnalyticsController.cs | 117 ++++ .../Controllers/AppointmentController.cs | 11 + .../Controllers/CrudControllerBase.cs | 137 ++++ .../Controllers/DoctorController.cs | 11 + .../Controllers/PatientController.cs | 11 + .../Controllers/SpecializationController.cs | 11 + .../Polyclinic.Api.Host.csproj | 25 + Polyclinic/Polyclinic.Api.Host/Program.cs | 84 +++ .../Properties/launchSettings.json | 41 ++ .../appsettings.Development.json | 8 + .../Polyclinic.Api.Host/appsettings.json | 9 + Polyclinic/Polyclinic.AppHost/AppHost.cs | 10 + .../Polyclinic.AppHost.csproj | 22 + .../Properties/launchSettings.json | 29 + .../appsettings.Development.json | 8 + .../Polyclinic.AppHost/appsettings.json | 9 + .../Analytics/MonthlyAppointmentStatsDto.cs | 15 + .../AppointmentCreateUpdateDto.cs | 17 + .../Appointments/AppointmentDto.cs | 19 + .../Doctors/DoctorCreateUpdateDto.cs | 17 + .../Doctors/DoctorDto.cs | 19 + .../IAnalyticsService.cs | 58 ++ .../IApplicationService.cs | 58 ++ .../Patients/PatientCreateUpdateDto.cs | 25 + .../Patients/PatientDto.cs | 27 + .../Polyclinic.Application.Contracts.csproj | 14 + .../SpecializationCreateUpdateDto.cs | 13 + .../Specializations/SpecializationDto.cs | 15 + .../Polyclinic.Application.csproj | 17 + .../PolyclinicProfile.cs | 29 + .../Services/AnalyticsSerivce.cs | 106 +++ .../Services/AppointmentService.cs | 69 ++ .../Services/DoctorService.cs | 62 ++ .../Services/PatientService.cs | 55 ++ .../Services/SpecializationService.cs | 55 ++ Polyclinic/Polyclinic.Domain/IRepository.cs | 55 ++ .../PolyclinicFixture.cs | 5 +- .../20260216193102_InitialCreate.Designer.cs | 627 ++++++++++++++++++ .../20260216193102_InitialCreate.cs | 206 ++++++ .../PolyclinicDbContextModelSnapshot.cs | 624 +++++++++++++++++ .../Polyclinic.Infrastructure.EfCore.csproj | 17 + .../PolyclinicDbContext.cs | 165 +++++ .../Repositories/AppointmentRepository.cs | 71 ++ .../Repositories/DoctorRepository.cs | 67 ++ .../Repositories/PatientRepository.cs | 63 ++ .../Repositories/SpecializationRepository.cs | 63 ++ .../Polyclinic.ServiceDefaults/Extensions.cs | 127 ++++ .../Polyclinic.ServiceDefaults.csproj | 22 + .../Polyclinic.Tests/PolyclinicTests.cs | 4 +- Polyclinic/Polyclinic.sln | 36 + 50 files changed, 3380 insertions(+), 5 deletions(-) create mode 100644 Polyclinic/Polyclinic.Api.Host/Controllers/AnalyticsController.cs create mode 100644 Polyclinic/Polyclinic.Api.Host/Controllers/AppointmentController.cs create mode 100644 Polyclinic/Polyclinic.Api.Host/Controllers/CrudControllerBase.cs create mode 100644 Polyclinic/Polyclinic.Api.Host/Controllers/DoctorController.cs create mode 100644 Polyclinic/Polyclinic.Api.Host/Controllers/PatientController.cs create mode 100644 Polyclinic/Polyclinic.Api.Host/Controllers/SpecializationController.cs create mode 100644 Polyclinic/Polyclinic.Api.Host/Polyclinic.Api.Host.csproj create mode 100644 Polyclinic/Polyclinic.Api.Host/Program.cs create mode 100644 Polyclinic/Polyclinic.Api.Host/Properties/launchSettings.json create mode 100644 Polyclinic/Polyclinic.Api.Host/appsettings.Development.json create mode 100644 Polyclinic/Polyclinic.Api.Host/appsettings.json create mode 100644 Polyclinic/Polyclinic.AppHost/AppHost.cs create mode 100644 Polyclinic/Polyclinic.AppHost/Polyclinic.AppHost.csproj create mode 100644 Polyclinic/Polyclinic.AppHost/Properties/launchSettings.json create mode 100644 Polyclinic/Polyclinic.AppHost/appsettings.Development.json create mode 100644 Polyclinic/Polyclinic.AppHost/appsettings.json create mode 100644 Polyclinic/Polyclinic.Application.Contracts/Analytics/MonthlyAppointmentStatsDto.cs create mode 100644 Polyclinic/Polyclinic.Application.Contracts/Appointments/AppointmentCreateUpdateDto.cs create mode 100644 Polyclinic/Polyclinic.Application.Contracts/Appointments/AppointmentDto.cs create mode 100644 Polyclinic/Polyclinic.Application.Contracts/Doctors/DoctorCreateUpdateDto.cs create mode 100644 Polyclinic/Polyclinic.Application.Contracts/Doctors/DoctorDto.cs create mode 100644 Polyclinic/Polyclinic.Application.Contracts/IAnalyticsService.cs create mode 100644 Polyclinic/Polyclinic.Application.Contracts/IApplicationService.cs create mode 100644 Polyclinic/Polyclinic.Application.Contracts/Patients/PatientCreateUpdateDto.cs create mode 100644 Polyclinic/Polyclinic.Application.Contracts/Patients/PatientDto.cs create mode 100644 Polyclinic/Polyclinic.Application.Contracts/Polyclinic.Application.Contracts.csproj create mode 100644 Polyclinic/Polyclinic.Application.Contracts/Specializations/SpecializationCreateUpdateDto.cs create mode 100644 Polyclinic/Polyclinic.Application.Contracts/Specializations/SpecializationDto.cs create mode 100644 Polyclinic/Polyclinic.Application/Polyclinic.Application.csproj create mode 100644 Polyclinic/Polyclinic.Application/PolyclinicProfile.cs create mode 100644 Polyclinic/Polyclinic.Application/Services/AnalyticsSerivce.cs create mode 100644 Polyclinic/Polyclinic.Application/Services/AppointmentService.cs create mode 100644 Polyclinic/Polyclinic.Application/Services/DoctorService.cs create mode 100644 Polyclinic/Polyclinic.Application/Services/PatientService.cs create mode 100644 Polyclinic/Polyclinic.Application/Services/SpecializationService.cs create mode 100644 Polyclinic/Polyclinic.Domain/IRepository.cs rename Polyclinic/{Polyclinic.Tests => Polyclinic.Domain}/PolyclinicFixture.cs (99%) create mode 100644 Polyclinic/Polyclinic.Infrastructure.EfCore/Migrations/20260216193102_InitialCreate.Designer.cs create mode 100644 Polyclinic/Polyclinic.Infrastructure.EfCore/Migrations/20260216193102_InitialCreate.cs create mode 100644 Polyclinic/Polyclinic.Infrastructure.EfCore/Migrations/PolyclinicDbContextModelSnapshot.cs create mode 100644 Polyclinic/Polyclinic.Infrastructure.EfCore/Polyclinic.Infrastructure.EfCore.csproj create mode 100644 Polyclinic/Polyclinic.Infrastructure.EfCore/PolyclinicDbContext.cs create mode 100644 Polyclinic/Polyclinic.Infrastructure.EfCore/Repositories/AppointmentRepository.cs create mode 100644 Polyclinic/Polyclinic.Infrastructure.EfCore/Repositories/DoctorRepository.cs create mode 100644 Polyclinic/Polyclinic.Infrastructure.EfCore/Repositories/PatientRepository.cs create mode 100644 Polyclinic/Polyclinic.Infrastructure.EfCore/Repositories/SpecializationRepository.cs create mode 100644 Polyclinic/Polyclinic.ServiceDefaults/Extensions.cs create mode 100644 Polyclinic/Polyclinic.ServiceDefaults/Polyclinic.ServiceDefaults.csproj diff --git a/Polyclinic/Polyclinic.Api.Host/Controllers/AnalyticsController.cs b/Polyclinic/Polyclinic.Api.Host/Controllers/AnalyticsController.cs new file mode 100644 index 000000000..ee88fceb5 --- /dev/null +++ b/Polyclinic/Polyclinic.Api.Host/Controllers/AnalyticsController.cs @@ -0,0 +1,117 @@ +using Microsoft.AspNetCore.Mvc; +using Polyclinic.Application.Contracts; +using Polyclinic.Application.Contracts.Analytics; +using Polyclinic.Application.Contracts.Appointments; +using Polyclinic.Application.Contracts.Doctors; +using Polyclinic.Application.Contracts.Patients; + +namespace Polyclinic.Api.Host.Controllers; + +/// +/// Контроллер для получения аналитических отчетов и выборок +/// +[ApiController] +[Route("api/[controller]")] +public class AnalyticsController(IAnalyticsService analyticsService) : ControllerBase +{ + /// + /// Получить врачей со стажем работы более указанного (по умолчанию 10 лет) + /// + /// Минимальный стаж в годах + [HttpGet("doctors/experienced")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> GetExperiencedDoctors([FromQuery] int minExperience = 10) + { + try + { + var result = await analyticsService.GetDoctorsWithExperienceAsync(minExperience); + return Ok(result); + } + catch (Exception ex) + { + return StatusCode(StatusCodes.Status500InternalServerError, new { error = ex.Message }); + } + } + + /// + /// Получить список пациентов, записанных к конкретному врачу, отсортированный по ФИО + /// + /// Идентификатор врача + [HttpGet("doctors/{doctorId}/patients")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> GetPatientsByDoctor(int doctorId) + { + try + { + var result = await analyticsService.GetPatientsByDoctorAsync(doctorId); + return Ok(result); + } + catch (Exception ex) + { + return StatusCode(StatusCodes.Status500InternalServerError, new { error = ex.Message }); + } + } + + /// + /// Получить статистику повторных приемов за указанный месяц + /// + /// Дата для определения месяца и года + [HttpGet("appointments/stats/monthly")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> GetMonthlyStats([FromQuery] DateTime date) + { + try + { + var result = await analyticsService.GetMonthlyRepeatStatsAsync(date); + return Ok(result); + } + catch (Exception ex) + { + return StatusCode(StatusCodes.Status500InternalServerError, new { error = ex.Message }); + } + } + + /// + /// Получить пациентов старше указанного возраста (по умолчанию 30), посетивших более одного врача + /// + /// Минимальный возраст + [HttpGet("patients/active-visitors")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> GetPatientsWithMultipleDoctors([FromQuery] int minAge = 30) + { + try + { + var result = await analyticsService.GetPatientsWithMultipleDoctorsAsync(minAge); + return Ok(result); + } + catch (Exception ex) + { + return StatusCode(StatusCodes.Status500InternalServerError, new { error = ex.Message }); + } + } + + /// + /// Получить список приемов в конкретном кабинете за месяц + /// + /// Номер кабинета + /// Дата для определения месяца выборки + [HttpGet("rooms/{roomNumber}/appointments")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> GetAppointmentsByRoom(string roomNumber, [FromQuery] DateTime date) + { + try + { + var result = await analyticsService.GetAppointmentsByRoomAsync(roomNumber, date); + return Ok(result); + } + catch (Exception ex) + { + return StatusCode(StatusCodes.Status500InternalServerError, new { error = ex.Message }); + } + } +} \ No newline at end of file diff --git a/Polyclinic/Polyclinic.Api.Host/Controllers/AppointmentController.cs b/Polyclinic/Polyclinic.Api.Host/Controllers/AppointmentController.cs new file mode 100644 index 000000000..ab46376ee --- /dev/null +++ b/Polyclinic/Polyclinic.Api.Host/Controllers/AppointmentController.cs @@ -0,0 +1,11 @@ +using Polyclinic.Application.Contracts; +using Polyclinic.Application.Contracts.Appointments; + +namespace Polyclinic.Api.Host.Controllers; + +/// +/// Контроллер для управления записями на прием +/// +public class AppointmentsController( + IApplicationService service) + : CrudControllerBase(service); \ No newline at end of file diff --git a/Polyclinic/Polyclinic.Api.Host/Controllers/CrudControllerBase.cs b/Polyclinic/Polyclinic.Api.Host/Controllers/CrudControllerBase.cs new file mode 100644 index 000000000..10f43ada2 --- /dev/null +++ b/Polyclinic/Polyclinic.Api.Host/Controllers/CrudControllerBase.cs @@ -0,0 +1,137 @@ +using Microsoft.AspNetCore.Mvc; +using Polyclinic.Application.Contracts; + +namespace Polyclinic.Api.Host.Controllers; + +/// +/// Базовый абстрактный контроллер, реализующий стандартные CRUD-операции +/// +/// Тип DTO для чтения +/// Тип DTO для создания и обновления +/// Тип идентификатора сущности +[ApiController] +[Route("api/[controller]")] +public abstract class CrudControllerBase( + IApplicationService service) + : ControllerBase + where TDto : class + where TCreateUpdateDto : class + where TKey : struct +{ + /// + /// Получает список всех сущностей + /// + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public virtual async Task>> GetAll() + { + try + { + var result = await service.GetAll(); + return Ok(result); + } + catch (Exception ex) + { + return StatusCode(StatusCodes.Status500InternalServerError, new { error = ex.Message }); + } + } + + /// + /// Получает сущность по идентификатору + /// + /// Идентификатор сущности + [HttpGet("{id}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public virtual async Task> Get(TKey id) + { + try + { + var result = await service.Get(id); + if (result == null) return NotFound(); + + return Ok(result); + } + catch (KeyNotFoundException ex) + { + return NotFound(new { error = ex.Message }); + } + catch (Exception ex) + { + return StatusCode(StatusCodes.Status500InternalServerError, new { error = ex.Message }); + } + } + + /// + /// Создает новую сущность + /// + /// DTO с данными для создания + [HttpPost] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public virtual async Task> Create([FromBody] TCreateUpdateDto dto) + { + try + { + var result = await service.Create(dto); + return CreatedAtAction(nameof(this.Create), result); + } + catch (KeyNotFoundException ex) + { + return NotFound(new { error = ex.Message }); + } + catch (Exception ex) + { + return StatusCode(StatusCodes.Status500InternalServerError, new { error = ex.Message }); + } + } + + /// + /// Обновляет существующую сущность + /// + /// Идентификатор обновляемой сущности + /// DTO с новыми данными + [HttpPut("{id}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public virtual async Task> Update(TKey id, [FromBody] TCreateUpdateDto dto) + { + try + { + var result = await service.Update(dto, id); + return Ok(result); + } + catch (KeyNotFoundException ex) + { + return NotFound(new { error = ex.Message }); + } + catch (Exception ex) + { + return StatusCode(StatusCodes.Status500InternalServerError, new { error = ex.Message }); + } + } + + /// + /// Удаляет сущность по идентификатору + /// + /// Идентификатор удаляемой сущности + [HttpDelete("{id}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public virtual async Task Delete(TKey id) + { + try + { + await service.Delete(id); + return NoContent(); + } + catch (Exception ex) + { + return StatusCode(StatusCodes.Status500InternalServerError, new { error = ex.Message }); + } + } +} \ No newline at end of file diff --git a/Polyclinic/Polyclinic.Api.Host/Controllers/DoctorController.cs b/Polyclinic/Polyclinic.Api.Host/Controllers/DoctorController.cs new file mode 100644 index 000000000..9def16d00 --- /dev/null +++ b/Polyclinic/Polyclinic.Api.Host/Controllers/DoctorController.cs @@ -0,0 +1,11 @@ +using Polyclinic.Application.Contracts; +using Polyclinic.Application.Contracts.Doctors; + +namespace Polyclinic.Api.Host.Controllers; + +/// +/// Контроллер для управления данными врачей +/// +public class DoctorController( + IApplicationService service) + : CrudControllerBase(service); \ No newline at end of file diff --git a/Polyclinic/Polyclinic.Api.Host/Controllers/PatientController.cs b/Polyclinic/Polyclinic.Api.Host/Controllers/PatientController.cs new file mode 100644 index 000000000..0d01d2a3d --- /dev/null +++ b/Polyclinic/Polyclinic.Api.Host/Controllers/PatientController.cs @@ -0,0 +1,11 @@ +using Polyclinic.Application.Contracts; +using Polyclinic.Application.Contracts.Patients; + +namespace Polyclinic.Api.Host.Controllers; + +/// +/// Контроллер для управления данными пациентов +/// +public class PatientsController( + IApplicationService service) + : CrudControllerBase(service); \ No newline at end of file diff --git a/Polyclinic/Polyclinic.Api.Host/Controllers/SpecializationController.cs b/Polyclinic/Polyclinic.Api.Host/Controllers/SpecializationController.cs new file mode 100644 index 000000000..3c3d95eb6 --- /dev/null +++ b/Polyclinic/Polyclinic.Api.Host/Controllers/SpecializationController.cs @@ -0,0 +1,11 @@ +using Polyclinic.Application.Contracts; +using Polyclinic.Application.Contracts.Specializations; + +namespace Polyclinic.Api.Host.Controllers; + +/// +/// Контроллер для управления справочником специализаций +/// +public class SpecializationController( + IApplicationService service) + : CrudControllerBase(service); \ No newline at end of file diff --git a/Polyclinic/Polyclinic.Api.Host/Polyclinic.Api.Host.csproj b/Polyclinic/Polyclinic.Api.Host/Polyclinic.Api.Host.csproj new file mode 100644 index 000000000..ef255a58a --- /dev/null +++ b/Polyclinic/Polyclinic.Api.Host/Polyclinic.Api.Host.csproj @@ -0,0 +1,25 @@ + + + + net8.0 + enable + enable + true + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + diff --git a/Polyclinic/Polyclinic.Api.Host/Program.cs b/Polyclinic/Polyclinic.Api.Host/Program.cs new file mode 100644 index 000000000..1b7bdf109 --- /dev/null +++ b/Polyclinic/Polyclinic.Api.Host/Program.cs @@ -0,0 +1,84 @@ +using Microsoft.EntityFrameworkCore; +using Polyclinic.Application; +using Polyclinic.Application.Contracts; +using Polyclinic.Application.Contracts.Appointments; +using Polyclinic.Application.Contracts.Doctors; +using Polyclinic.Application.Contracts.Patients; +using Polyclinic.Application.Contracts.Specializations; +using Polyclinic.Application.Services; +using Polyclinic.Domain; +using Polyclinic.Domain.Entities; +using Polyclinic.Infrastructure.EfCore; +using Polyclinic.Infrastructure.EfCore.Repositories; +using Polyclinic.ServiceDefaults; +using System.Text.Json.Serialization; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddAutoMapper(config => +{ + config.AddProfile(new PolyclinicProfile()); +}); + +builder.Services.AddSingleton(); + +builder.AddServiceDefaults(); + +builder.Services.AddTransient, SpecializationRepository>(); +builder.Services.AddTransient, PatientRepository>(); +builder.Services.AddTransient, DoctorRepository>(); +builder.Services.AddTransient, AppointmentRepository>(); + +builder.Services.AddScoped(); +builder.Services.AddScoped, SpecializationService>(); +builder.Services.AddScoped, PatientService>(); +builder.Services.AddScoped, DoctorService>(); +builder.Services.AddScoped, AppointmentService>(); + +builder.Services.AddControllers() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + }); builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(c => +{ + c.UseInlineDefinitionsForEnums(); + + var assemblies = AppDomain.CurrentDomain.GetAssemblies() + .Where(a => a.GetName().Name!.StartsWith("Polyclinic")) + .Distinct(); + + foreach (var assembly in assemblies) + { + var xmlFile = $"{assembly.GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + if (File.Exists(xmlPath)) + c.IncludeXmlComments(xmlPath); + } + + c.UseInlineDefinitionsForEnums(); +}); + +builder.AddSqlServerDbContext("ConnectionString"); + +var app = builder.Build(); + +app.MapDefaultEndpoints(); + +if (app.Environment.IsDevelopment()) +{ + using var scope = app.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + await db.Database.MigrateAsync(); + + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/Polyclinic/Polyclinic.Api.Host/Properties/launchSettings.json b/Polyclinic/Polyclinic.Api.Host/Properties/launchSettings.json new file mode 100644 index 000000000..6c75e264a --- /dev/null +++ b/Polyclinic/Polyclinic.Api.Host/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:20801", + "sslPort": 44366 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5087", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7184;http://localhost:5087", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Polyclinic/Polyclinic.Api.Host/appsettings.Development.json b/Polyclinic/Polyclinic.Api.Host/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/Polyclinic/Polyclinic.Api.Host/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Polyclinic/Polyclinic.Api.Host/appsettings.json b/Polyclinic/Polyclinic.Api.Host/appsettings.json new file mode 100644 index 000000000..10f68b8c8 --- /dev/null +++ b/Polyclinic/Polyclinic.Api.Host/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Polyclinic/Polyclinic.AppHost/AppHost.cs b/Polyclinic/Polyclinic.AppHost/AppHost.cs new file mode 100644 index 000000000..9cb2f758c --- /dev/null +++ b/Polyclinic/Polyclinic.AppHost/AppHost.cs @@ -0,0 +1,10 @@ +var builder = DistributedApplication.CreateBuilder(args); + +var sqlDb = builder.AddSqlServer("polyclinic-sql-server") + .AddDatabase("PolyclinicDb"); + +builder.AddProject("polyclinic-api-host") + .WithReference(sqlDb, "ConnectionString") + .WaitFor(sqlDb); + +builder.Build().Run(); \ No newline at end of file diff --git a/Polyclinic/Polyclinic.AppHost/Polyclinic.AppHost.csproj b/Polyclinic/Polyclinic.AppHost/Polyclinic.AppHost.csproj new file mode 100644 index 000000000..2d8eef84f --- /dev/null +++ b/Polyclinic/Polyclinic.AppHost/Polyclinic.AppHost.csproj @@ -0,0 +1,22 @@ + + + + + + Exe + net8.0 + enable + enable + 8caaa9c7-a496-41cb-a0fe-a05874e7d079 + + + + + + + + + + + + diff --git a/Polyclinic/Polyclinic.AppHost/Properties/launchSettings.json b/Polyclinic/Polyclinic.AppHost/Properties/launchSettings.json new file mode 100644 index 000000000..e321feca2 --- /dev/null +++ b/Polyclinic/Polyclinic.AppHost/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17108;http://localhost:15280", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21020", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22277" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15280", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19195", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20298" + } + } + } +} diff --git a/Polyclinic/Polyclinic.AppHost/appsettings.Development.json b/Polyclinic/Polyclinic.AppHost/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/Polyclinic/Polyclinic.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Polyclinic/Polyclinic.AppHost/appsettings.json b/Polyclinic/Polyclinic.AppHost/appsettings.json new file mode 100644 index 000000000..31c092aa4 --- /dev/null +++ b/Polyclinic/Polyclinic.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/Polyclinic/Polyclinic.Application.Contracts/Analytics/MonthlyAppointmentStatsDto.cs b/Polyclinic/Polyclinic.Application.Contracts/Analytics/MonthlyAppointmentStatsDto.cs new file mode 100644 index 000000000..0d0719394 --- /dev/null +++ b/Polyclinic/Polyclinic.Application.Contracts/Analytics/MonthlyAppointmentStatsDto.cs @@ -0,0 +1,15 @@ +namespace Polyclinic.Application.Contracts.Analytics; + +/// +/// DTO со статистикой приемов за месяц +/// +/// Год статистики +/// Месяц статистики +/// Количество повторных приемов +/// Общее количество приемов +public record MonthlyAppointmentStatsDto( + int Year, + int Month, + int RepeatAppointmentCount, + int TotalAppointmentCount +); \ No newline at end of file diff --git a/Polyclinic/Polyclinic.Application.Contracts/Appointments/AppointmentCreateUpdateDto.cs b/Polyclinic/Polyclinic.Application.Contracts/Appointments/AppointmentCreateUpdateDto.cs new file mode 100644 index 000000000..839797695 --- /dev/null +++ b/Polyclinic/Polyclinic.Application.Contracts/Appointments/AppointmentCreateUpdateDto.cs @@ -0,0 +1,17 @@ +namespace Polyclinic.Application.Contracts.Appointments; + +/// +/// DTO для создания и обновления записи на прием +/// +/// Дата и время приема +/// Номер кабинета +/// Признак повторного приема +/// Идентификатор пациента +/// Идентификатор врача +public record AppointmentCreateUpdateDto( + DateTime AppointmentDateTime, + string RoomNumber, + bool IsRepeat, + int PatientId, + int DoctorId +); \ No newline at end of file diff --git a/Polyclinic/Polyclinic.Application.Contracts/Appointments/AppointmentDto.cs b/Polyclinic/Polyclinic.Application.Contracts/Appointments/AppointmentDto.cs new file mode 100644 index 000000000..816c8ed39 --- /dev/null +++ b/Polyclinic/Polyclinic.Application.Contracts/Appointments/AppointmentDto.cs @@ -0,0 +1,19 @@ +namespace Polyclinic.Application.Contracts.Appointments; + +/// +/// DTO записи на прием для чтения +/// +/// Уникальный идентификатор записи +/// Дата и время приема +/// Номер кабинета +/// Признак повторного приема +/// Идентификатор пациента +/// Идентификатор врача +public record AppointmentDto( + int Id, + DateTime AppointmentDateTime, + string RoomNumber, + bool IsRepeat, + int PatientId, + int DoctorId +); \ No newline at end of file diff --git a/Polyclinic/Polyclinic.Application.Contracts/Doctors/DoctorCreateUpdateDto.cs b/Polyclinic/Polyclinic.Application.Contracts/Doctors/DoctorCreateUpdateDto.cs new file mode 100644 index 000000000..beb1f03e0 --- /dev/null +++ b/Polyclinic/Polyclinic.Application.Contracts/Doctors/DoctorCreateUpdateDto.cs @@ -0,0 +1,17 @@ +namespace Polyclinic.Application.Contracts.Doctors; + +/// +/// DTO для создания и обновления врача +/// +/// Номер паспорта +/// ФИО врача +/// Дата рождения +/// Идентификатор специализации +/// Стаж работы (лет) +public record DoctorCreateUpdateDto( + string PassportNumber, + string FullName, + DateTime BirthDate, + int SpecializationId, + int ExperienceYears +); \ No newline at end of file diff --git a/Polyclinic/Polyclinic.Application.Contracts/Doctors/DoctorDto.cs b/Polyclinic/Polyclinic.Application.Contracts/Doctors/DoctorDto.cs new file mode 100644 index 000000000..54a669b76 --- /dev/null +++ b/Polyclinic/Polyclinic.Application.Contracts/Doctors/DoctorDto.cs @@ -0,0 +1,19 @@ +namespace Polyclinic.Application.Contracts.Doctors; + +/// +/// DTO врача для чтения +/// +/// Уникальный идентификатор врача +/// Номер паспорта +/// ФИО врача +/// Дата рождения +/// Идентификатор специализации +/// Стаж работы (лет) +public record DoctorDto( + int Id, + string PassportNumber, + string FullName, + DateTime BirthDate, + int SpecializationId, + int ExperienceYears +); \ No newline at end of file diff --git a/Polyclinic/Polyclinic.Application.Contracts/IAnalyticsService.cs b/Polyclinic/Polyclinic.Application.Contracts/IAnalyticsService.cs new file mode 100644 index 000000000..b00122bb1 --- /dev/null +++ b/Polyclinic/Polyclinic.Application.Contracts/IAnalyticsService.cs @@ -0,0 +1,58 @@ +using Polyclinic.Application.Contracts.Analytics; +using Polyclinic.Application.Contracts.Appointments; +using Polyclinic.Application.Contracts.Doctors; +using Polyclinic.Application.Contracts.Patients; + +namespace Polyclinic.Application.Contracts; + +/// +/// Сервис для получения аналитических выборок и отчетов +/// +public interface IAnalyticsService +{ + /// + /// Возвращает список врачей со стажем работы более указанного количества лет + /// + /// Минимальный стаж в годах (по умолчанию 10) + /// + /// Список врачей, удовлетворяющих условию + /// + public Task> GetDoctorsWithExperienceAsync(int minExperienceYears = 10); + + /// + /// Возвращает список пациентов, записанных к конкретному врачу, отсортированный по ФИО + /// + /// Идентификатор врача + /// + /// Отсортированный список пациентов + /// + public Task> GetPatientsByDoctorAsync(int doctorId); + + /// + /// Возвращает статистику по повторным приемам за указанный месяц + /// + /// Дата, определяющая месяц и год выборки + /// + /// Статистика по приемам + /// + public Task GetMonthlyRepeatStatsAsync(DateTime targetDate); + + /// + /// Возвращает пациентов старше определенного возраста, посетивших более одного врача, отсортированных по дате рождения + /// + /// Минимальный возраст пациента (по умолчанию 30) + /// + /// Список пациентов + /// + public Task> GetPatientsWithMultipleDoctorsAsync(int minAge = 30); + + /// + /// Возвращает список приемов в указанном кабинете за текущий месяц (относительно переданной даты) + /// + /// Номер кабинета + /// Дата для определения месяца выборки + /// + /// Список приемов + /// + public Task> GetAppointmentsByRoomAsync(string roomNumber, DateTime targetDate); +} \ No newline at end of file diff --git a/Polyclinic/Polyclinic.Application.Contracts/IApplicationService.cs b/Polyclinic/Polyclinic.Application.Contracts/IApplicationService.cs new file mode 100644 index 000000000..8eaa7c238 --- /dev/null +++ b/Polyclinic/Polyclinic.Application.Contracts/IApplicationService.cs @@ -0,0 +1,58 @@ +namespace Polyclinic.Application.Contracts; + +/// +/// Общий интерфейс сервиса приложения для выполнения CRUD-операций над DTO +/// +/// Тип DTO для чтения данных +/// Тип DTO для создания и обновления данных +/// Тип первичного ключа сущности +public interface IApplicationService + where TDto : class + where TCreateUpdateDto : class + where TKey : struct +{ + /// + /// Создаёт новую запись на основе DTO + /// + /// DTO с данными для создания + /// + /// Созданный объект DTO с заполненным идентификатором + /// + public Task Create(TCreateUpdateDto dto); + + /// + /// Получает запись по идентификатору + /// + /// Идентификатор записи + /// + /// DTO записи, если она найдена; иначе null + /// + public Task Get(TKey dtoId); + + /// + /// Получает список всех записей + /// + /// + /// Коллекция всех записей DTO + /// + public Task> GetAll(); + + /// + /// Обновляет существующую запись + /// + /// DTO с обновлёнными данными + /// Идентификатор обновляемой записи + /// + /// Обновлённый объект DTO + /// + public Task Update(TCreateUpdateDto dto, TKey dtoId); + + /// + /// Удаляет запись по идентификатору + /// + /// Идентификатор удаляемой записи + /// + /// true, если запись была найдена и удалена; иначе false + /// + public Task Delete(TKey dtoId); +} \ No newline at end of file diff --git a/Polyclinic/Polyclinic.Application.Contracts/Patients/PatientCreateUpdateDto.cs b/Polyclinic/Polyclinic.Application.Contracts/Patients/PatientCreateUpdateDto.cs new file mode 100644 index 000000000..51f9e71dd --- /dev/null +++ b/Polyclinic/Polyclinic.Application.Contracts/Patients/PatientCreateUpdateDto.cs @@ -0,0 +1,25 @@ +using Polyclinic.Domain.Enums; + +namespace Polyclinic.Application.Contracts.Patients; + +/// +/// DTO для создания и обновления пациента +/// +/// Номер паспорта +/// ФИО пациента +/// Пол пациента +/// Дата рождения +/// Адрес проживания +/// Группа крови +/// Резус-фактор +/// Контактный телефон +public record PatientCreateUpdateDto( + string PassportNumber, + string FullName, + Gender Gender, + DateTime BirthDate, + string Address, + BloodGroup BloodGroup, + RhFactor RhFactor, + string PhoneNumber +); \ No newline at end of file diff --git a/Polyclinic/Polyclinic.Application.Contracts/Patients/PatientDto.cs b/Polyclinic/Polyclinic.Application.Contracts/Patients/PatientDto.cs new file mode 100644 index 000000000..8332c45d6 --- /dev/null +++ b/Polyclinic/Polyclinic.Application.Contracts/Patients/PatientDto.cs @@ -0,0 +1,27 @@ +using Polyclinic.Domain.Enums; + +namespace Polyclinic.Application.Contracts.Patients; + +/// +/// DTO пациента для чтения +/// +/// Уникальный идентификатор пациента +/// Номер паспорта +/// ФИО пациента +/// Пол пациента +/// Дата рождения +/// Адрес проживания +/// Группа крови +/// Резус-фактор +/// Контактный телефон +public record PatientDto( + int Id, + string PassportNumber, + string FullName, + Gender Gender, + DateTime BirthDate, + string Address, + BloodGroup BloodGroup, + RhFactor RhFactor, + string PhoneNumber +); \ No newline at end of file diff --git a/Polyclinic/Polyclinic.Application.Contracts/Polyclinic.Application.Contracts.csproj b/Polyclinic/Polyclinic.Application.Contracts/Polyclinic.Application.Contracts.csproj new file mode 100644 index 000000000..14a830ef0 --- /dev/null +++ b/Polyclinic/Polyclinic.Application.Contracts/Polyclinic.Application.Contracts.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + true + + + + + + + diff --git a/Polyclinic/Polyclinic.Application.Contracts/Specializations/SpecializationCreateUpdateDto.cs b/Polyclinic/Polyclinic.Application.Contracts/Specializations/SpecializationCreateUpdateDto.cs new file mode 100644 index 000000000..3df679b9d --- /dev/null +++ b/Polyclinic/Polyclinic.Application.Contracts/Specializations/SpecializationCreateUpdateDto.cs @@ -0,0 +1,13 @@ +namespace Polyclinic.Application.Contracts.Specializations; + +/// +/// DTO для создания и обновления специализации +/// +/// Название специализации +/// Описание специализации +/// Код специализации +public record SpecializationCreateUpdateDto( + string Name, + string Description, + string Code +); \ No newline at end of file diff --git a/Polyclinic/Polyclinic.Application.Contracts/Specializations/SpecializationDto.cs b/Polyclinic/Polyclinic.Application.Contracts/Specializations/SpecializationDto.cs new file mode 100644 index 000000000..0c25f8223 --- /dev/null +++ b/Polyclinic/Polyclinic.Application.Contracts/Specializations/SpecializationDto.cs @@ -0,0 +1,15 @@ +namespace Polyclinic.Application.Contracts.Specializations; + +/// +/// DTO специализации для чтения +/// +/// Уникальный идентификатор специализации +/// Название специализации +/// Описание специализации +/// Код специализации +public record SpecializationDto( + int Id, + string Name, + string Description, + string Code +); \ No newline at end of file diff --git a/Polyclinic/Polyclinic.Application/Polyclinic.Application.csproj b/Polyclinic/Polyclinic.Application/Polyclinic.Application.csproj new file mode 100644 index 000000000..14b3bad96 --- /dev/null +++ b/Polyclinic/Polyclinic.Application/Polyclinic.Application.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + diff --git a/Polyclinic/Polyclinic.Application/PolyclinicProfile.cs b/Polyclinic/Polyclinic.Application/PolyclinicProfile.cs new file mode 100644 index 000000000..cc66113ad --- /dev/null +++ b/Polyclinic/Polyclinic.Application/PolyclinicProfile.cs @@ -0,0 +1,29 @@ +using AutoMapper; +using Polyclinic.Application.Contracts.Appointments; +using Polyclinic.Application.Contracts.Doctors; +using Polyclinic.Application.Contracts.Patients; +using Polyclinic.Application.Contracts.Specializations; +using Polyclinic.Domain.Entities; + +namespace Polyclinic.Application; + +/// +/// Профиль маппинга для преобразования данных между доменными сущностями и DTO +/// +public class PolyclinicProfile : Profile +{ + public PolyclinicProfile() + { + CreateMap(); + CreateMap(); + + CreateMap(); + CreateMap(); + + CreateMap(); + CreateMap(); + + CreateMap(); + CreateMap(); + } +} \ No newline at end of file diff --git a/Polyclinic/Polyclinic.Application/Services/AnalyticsSerivce.cs b/Polyclinic/Polyclinic.Application/Services/AnalyticsSerivce.cs new file mode 100644 index 000000000..208a5ce9b --- /dev/null +++ b/Polyclinic/Polyclinic.Application/Services/AnalyticsSerivce.cs @@ -0,0 +1,106 @@ +using AutoMapper; +using Polyclinic.Application.Contracts; +using Polyclinic.Application.Contracts.Analytics; +using Polyclinic.Application.Contracts.Appointments; +using Polyclinic.Application.Contracts.Doctors; +using Polyclinic.Application.Contracts.Patients; +using Polyclinic.Domain; +using Polyclinic.Domain.Entities; + +namespace Polyclinic.Application.Services; + +/// +/// Сервис для получения аналитических данных и отчетов +/// +public class AnalyticsService( + IRepository doctorRepository, + IRepository appointmentRepository, + IMapper mapper) : IAnalyticsService +{ + /// + public async Task> GetDoctorsWithExperienceAsync(int minExperienceYears = 10) + { + var doctors = await doctorRepository.ReadAll(); + + var filteredDoctors = doctors + .Where(d => d.ExperienceYears >= minExperienceYears) + .ToList(); + + return mapper.Map>(filteredDoctors); + } + + /// + public async Task> GetPatientsByDoctorAsync(int doctorId) + { + var appointments = await appointmentRepository.ReadAll(); + + var patients = appointments + .Where(a => a.DoctorId == doctorId && a.Patient != null) + .Select(a => a.Patient!) + .DistinctBy(p => p.Id) + .OrderBy(p => p.FullName) + .ToList(); + + return mapper.Map>(patients); + } + + /// + public async Task GetMonthlyRepeatStatsAsync(DateTime targetDate) + { + var appointments = await appointmentRepository.ReadAll(); + + var monthlyAppointments = appointments + .Where(a => a.AppointmentDateTime.Year == targetDate.Year + && a.AppointmentDateTime.Month == targetDate.Month) + .ToList(); + + var totalCount = monthlyAppointments.Count; + var repeatCount = monthlyAppointments.Count(a => a.IsRepeat); + + return new MonthlyAppointmentStatsDto( + Year: targetDate.Year, + Month: targetDate.Month, + RepeatAppointmentCount: repeatCount, + TotalAppointmentCount: totalCount + ); + } + + /// + public async Task> GetPatientsWithMultipleDoctorsAsync(int minAge = 30) + { + var appointments = await appointmentRepository.ReadAll(); + var today = DateTime.Today; + + var resultPatients = appointments + .Where(a => a.Patient != null) + .GroupBy(a => a.Patient) + .Where(g => + { + var patient = g.Key!; + var isOldEnough = patient.GetAge(today) > minAge; + + var hasMultipleDoctors = g.Select(a => a.DoctorId).Distinct().Count() > 1; + + return isOldEnough && hasMultipleDoctors; + }) + .Select(g => g.Key!) + .OrderBy(p => p.BirthDate) + .ToList(); + + return mapper.Map>(resultPatients); + } + + /// + public async Task> GetAppointmentsByRoomAsync(string roomNumber, DateTime targetDate) + { + var appointments = await appointmentRepository.ReadAll(); + + var filtered = appointments + .Where(a => a.RoomNumber == roomNumber + && a.AppointmentDateTime.Year == targetDate.Year + && a.AppointmentDateTime.Month == targetDate.Month) + .ToList(); + + return mapper.Map>(filtered); + } +} \ No newline at end of file diff --git a/Polyclinic/Polyclinic.Application/Services/AppointmentService.cs b/Polyclinic/Polyclinic.Application/Services/AppointmentService.cs new file mode 100644 index 000000000..58e151b61 --- /dev/null +++ b/Polyclinic/Polyclinic.Application/Services/AppointmentService.cs @@ -0,0 +1,69 @@ +using AutoMapper; +using Polyclinic.Application.Contracts; +using Polyclinic.Application.Contracts.Appointments; +using Polyclinic.Domain; +using Polyclinic.Domain.Entities; + +namespace Polyclinic.Application.Services; + +/// +/// Сервис для управления записями на прием +/// +public class AppointmentService( + IRepository appointmentRepository, + IRepository doctorRepository, + IRepository patientRepository, + IMapper mapper) + : IApplicationService +{ + /// + public async Task Create(AppointmentCreateUpdateDto dto) + { + _ = await patientRepository.Read(dto.PatientId) + ?? throw new KeyNotFoundException($"Пациент с id {dto.PatientId} не найден."); + + _ = await doctorRepository.Read(dto.DoctorId) + ?? throw new KeyNotFoundException($"Врач с id {dto.DoctorId} не найден."); + + var entity = mapper.Map(dto); + var result = await appointmentRepository.Create(entity); + return mapper.Map(result); + } + + /// + public async Task Get(int dtoId) + { + var entity = await appointmentRepository.Read(dtoId) + ?? throw new KeyNotFoundException($"Запись на прием с id {dtoId} не найдена."); + + return mapper.Map(entity); + } + + /// + public async Task> GetAll() + { + var entities = await appointmentRepository.ReadAll(); + return mapper.Map>(entities); + } + + /// + public async Task Update(AppointmentCreateUpdateDto dto, int dtoId) + { + _ = await patientRepository.Read(dto.PatientId) + ?? throw new KeyNotFoundException($"Пациент с id {dto.PatientId} не найден."); + + _ = await doctorRepository.Read(dto.DoctorId) + ?? throw new KeyNotFoundException($"Врач с id {dto.DoctorId} не найден."); + + var entity = await appointmentRepository.Read(dtoId) + ?? throw new KeyNotFoundException($"Запись на прием с id {dtoId} не найдена."); + + mapper.Map(dto, entity); + + var result = await appointmentRepository.Update(entity); + return mapper.Map(result); + } + + /// + public async Task Delete(int dtoId) => await appointmentRepository.Delete(dtoId); +} \ No newline at end of file diff --git a/Polyclinic/Polyclinic.Application/Services/DoctorService.cs b/Polyclinic/Polyclinic.Application/Services/DoctorService.cs new file mode 100644 index 000000000..cc6f59cf9 --- /dev/null +++ b/Polyclinic/Polyclinic.Application/Services/DoctorService.cs @@ -0,0 +1,62 @@ +using AutoMapper; +using Polyclinic.Application.Contracts; +using Polyclinic.Application.Contracts.Doctors; +using Polyclinic.Domain; +using Polyclinic.Domain.Entities; + +namespace Polyclinic.Application.Services; + +/// +/// Сервис для управления врачами +/// +public class DoctorService( + IRepository doctorRepository, + IRepository specializationRepository, + IMapper mapper) + : IApplicationService +{ + /// + public async Task Create(DoctorCreateUpdateDto dto) + { + _ = await specializationRepository.Read(dto.SpecializationId) + ?? throw new KeyNotFoundException($"Специализация с id {dto.SpecializationId} не найдена."); + + var entity = mapper.Map(dto); + var result = await doctorRepository.Create(entity); + return mapper.Map(result); + } + + /// + public async Task Get(int dtoId) + { + var entity = await doctorRepository.Read(dtoId) + ?? throw new KeyNotFoundException($"Врач с id {dtoId} не найден."); + + return mapper.Map(entity); + } + + /// + public async Task> GetAll() + { + var entities = await doctorRepository.ReadAll(); + return mapper.Map>(entities); + } + + /// + public async Task Update(DoctorCreateUpdateDto dto, int dtoId) + { + _ = await specializationRepository.Read(dto.SpecializationId) + ?? throw new KeyNotFoundException($"Специализация с id {dto.SpecializationId} не найдена."); + + var entity = await doctorRepository.Read(dtoId) + ?? throw new KeyNotFoundException($"Врач с id {dtoId} не найден."); + + mapper.Map(dto, entity); + + var result = await doctorRepository.Update(entity); + return mapper.Map(result); + } + + /// + public async Task Delete(int dtoId) => await doctorRepository.Delete(dtoId); +} \ No newline at end of file diff --git a/Polyclinic/Polyclinic.Application/Services/PatientService.cs b/Polyclinic/Polyclinic.Application/Services/PatientService.cs new file mode 100644 index 000000000..4b04075bd --- /dev/null +++ b/Polyclinic/Polyclinic.Application/Services/PatientService.cs @@ -0,0 +1,55 @@ +using AutoMapper; +using Polyclinic.Application.Contracts; +using Polyclinic.Application.Contracts.Patients; +using Polyclinic.Domain; +using Polyclinic.Domain.Entities; + +namespace Polyclinic.Application.Services; + +/// +/// Сервис для управления пациентами +/// +public class PatientService( + IRepository repository, + IMapper mapper) + : IApplicationService +{ + /// + public async Task Create(PatientCreateUpdateDto dto) + { + var entity = mapper.Map(dto); + var result = await repository.Create(entity); + return mapper.Map(result); + } + + /// + public async Task Get(int dtoId) + { + var entity = await repository.Read(dtoId) + ?? throw new KeyNotFoundException($"Пациент с id {dtoId} не найден."); + + return mapper.Map(entity); + } + + /// + public async Task> GetAll() + { + var entities = await repository.ReadAll(); + return mapper.Map>(entities); + } + + /// + public async Task Update(PatientCreateUpdateDto dto, int dtoId) + { + var entity = await repository.Read(dtoId) + ?? throw new KeyNotFoundException($"Пациент с id {dtoId} не найден."); + + mapper.Map(dto, entity); + + var result = await repository.Update(entity); + return mapper.Map(result); + } + + /// + public async Task Delete(int dtoId) => await repository.Delete(dtoId); +} \ No newline at end of file diff --git a/Polyclinic/Polyclinic.Application/Services/SpecializationService.cs b/Polyclinic/Polyclinic.Application/Services/SpecializationService.cs new file mode 100644 index 000000000..e698a3d6c --- /dev/null +++ b/Polyclinic/Polyclinic.Application/Services/SpecializationService.cs @@ -0,0 +1,55 @@ +using AutoMapper; +using Polyclinic.Application.Contracts; +using Polyclinic.Application.Contracts.Specializations; +using Polyclinic.Domain; +using Polyclinic.Domain.Entities; + +namespace Polyclinic.Application.Services; + +/// +/// Сервис для управления специализациями +/// +public class SpecializationService( + IRepository repository, + IMapper mapper) + : IApplicationService +{ + /// + public async Task Create(SpecializationCreateUpdateDto dto) + { + var entity = mapper.Map(dto); + var result = await repository.Create(entity); + return mapper.Map(result); + } + + /// + public async Task Get(int dtoId) + { + var entity = await repository.Read(dtoId) + ?? throw new KeyNotFoundException($"Специализация с id {dtoId} не найдена."); + + return mapper.Map(entity); + } + + /// + public async Task> GetAll() + { + var entities = await repository.ReadAll(); + return mapper.Map>(entities); + } + + /// + public async Task Update(SpecializationCreateUpdateDto dto, int dtoId) + { + var entity = await repository.Read(dtoId) + ?? throw new KeyNotFoundException($"Специализация с id {dtoId} не найдена."); + + mapper.Map(dto, entity); + + var result = await repository.Update(entity); + return mapper.Map(result); + } + + /// + public async Task Delete(int dtoId) => await repository.Delete(dtoId); +} \ No newline at end of file diff --git a/Polyclinic/Polyclinic.Domain/IRepository.cs b/Polyclinic/Polyclinic.Domain/IRepository.cs new file mode 100644 index 000000000..4f4dc5f08 --- /dev/null +++ b/Polyclinic/Polyclinic.Domain/IRepository.cs @@ -0,0 +1,55 @@ +namespace Polyclinic.Domain; + +/// +/// Интерфейс репозитория, инкапсулирующий CRUD-операции над сущностью доменной модели +/// +/// Тип доменной сущности, над которой выполняются операции репозитория +/// Тип первичного ключа (идентификатора) сущности +public interface IRepository + where TEntity : class + where TKey : struct +{ + /// + /// Создаёт новую сущность и сохраняет её в источнике данных + /// + /// Экземпляр новой сущности для сохранения + /// + /// Созданная сущность + /// + public Task Create(TEntity entity); + + /// + /// Возвращает сущность по её идентификатору + /// + /// Идентификатор искомой сущности + /// + /// Сущность, если она найдена; иначе null + /// + public Task Read(TKey entityId); + + /// + /// Возвращает полный список сущностей данного типа + /// + /// + /// Список сущностей + /// + public Task> ReadAll(); + + /// + /// Обновляет существующую сущность в источнике данных + /// + /// Сущность с актуальными значениями полей + /// + /// Обновлённая сущность + /// + public Task Update(TEntity entity); + + /// + /// Удаляет сущность по её идентификатору + /// + /// Идентификатор удаляемой сущности + /// + /// true, если сущность была найдена и удалена; иначе false + /// + public Task Delete(TKey entityId); +} \ No newline at end of file diff --git a/Polyclinic/Polyclinic.Tests/PolyclinicFixture.cs b/Polyclinic/Polyclinic.Domain/PolyclinicFixture.cs similarity index 99% rename from Polyclinic/Polyclinic.Tests/PolyclinicFixture.cs rename to Polyclinic/Polyclinic.Domain/PolyclinicFixture.cs index 0dd92b41b..e7b6ea4b1 100644 --- a/Polyclinic/Polyclinic.Tests/PolyclinicFixture.cs +++ b/Polyclinic/Polyclinic.Domain/PolyclinicFixture.cs @@ -1,7 +1,7 @@ using Polyclinic.Domain.Entities; using Polyclinic.Domain.Enums; -namespace Polyclinic.Tests; +namespace Polyclinic.Domain; /// /// Фикстура с тестовыми данными для поликлиники @@ -19,9 +19,6 @@ public PolyclinicFixture() Doctors = GetDoctors(); Patients = GetPatients(); Appointments = GetAppointments(); - - LinkDoctorsWithSpecializations(); - LinkAppointmentsWithDoctorsAndPatients(); } private static List GetSpecializations() => diff --git a/Polyclinic/Polyclinic.Infrastructure.EfCore/Migrations/20260216193102_InitialCreate.Designer.cs b/Polyclinic/Polyclinic.Infrastructure.EfCore/Migrations/20260216193102_InitialCreate.Designer.cs new file mode 100644 index 000000000..f8d283b86 --- /dev/null +++ b/Polyclinic/Polyclinic.Infrastructure.EfCore/Migrations/20260216193102_InitialCreate.Designer.cs @@ -0,0 +1,627 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Polyclinic.Infrastructure.EfCore; + +#nullable disable + +namespace Polyclinic.Infrastructure.EfCore.Migrations +{ + [DbContext(typeof(PolyclinicDbContext))] + [Migration("20260216193102_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.13") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Polyclinic.Domain.Entities.Appointment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AppointmentDateTime") + .HasColumnType("datetime2"); + + b.Property("DoctorId") + .HasColumnType("int"); + + b.Property("IsRepeat") + .HasColumnType("bit"); + + b.Property("PatientId") + .HasColumnType("int"); + + b.Property("RoomNumber") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.HasKey("Id"); + + b.HasIndex("DoctorId"); + + b.HasIndex("PatientId"); + + b.ToTable("Appointments"); + + b.HasData( + new + { + Id = 1, + AppointmentDateTime = new DateTime(2026, 2, 5, 10, 0, 0, 0, DateTimeKind.Unspecified), + DoctorId = 1, + IsRepeat = false, + PatientId = 1, + RoomNumber = "101" + }, + new + { + Id = 2, + AppointmentDateTime = new DateTime(2026, 2, 5, 11, 0, 0, 0, DateTimeKind.Unspecified), + DoctorId = 1, + IsRepeat = true, + PatientId = 2, + RoomNumber = "101" + }, + new + { + Id = 3, + AppointmentDateTime = new DateTime(2026, 2, 10, 14, 30, 0, 0, DateTimeKind.Unspecified), + DoctorId = 2, + IsRepeat = false, + PatientId = 3, + RoomNumber = "202" + }, + new + { + Id = 4, + AppointmentDateTime = new DateTime(2026, 2, 15, 9, 15, 0, 0, DateTimeKind.Unspecified), + DoctorId = 3, + IsRepeat = false, + PatientId = 4, + RoomNumber = "303" + }, + new + { + Id = 5, + AppointmentDateTime = new DateTime(2026, 2, 12, 13, 30, 0, 0, DateTimeKind.Unspecified), + DoctorId = 1, + IsRepeat = false, + PatientId = 8, + RoomNumber = "101" + }, + new + { + Id = 6, + AppointmentDateTime = new DateTime(2026, 2, 16, 9, 30, 0, 0, DateTimeKind.Unspecified), + DoctorId = 1, + IsRepeat = false, + PatientId = 11, + RoomNumber = "101" + }, + new + { + Id = 7, + AppointmentDateTime = new DateTime(2026, 1, 15, 10, 0, 0, 0, DateTimeKind.Unspecified), + DoctorId = 2, + IsRepeat = true, + PatientId = 6, + RoomNumber = "101" + }, + new + { + Id = 8, + AppointmentDateTime = new DateTime(2026, 1, 20, 11, 0, 0, 0, DateTimeKind.Unspecified), + DoctorId = 2, + IsRepeat = true, + PatientId = 7, + RoomNumber = "202" + }, + new + { + Id = 9, + AppointmentDateTime = new DateTime(2026, 1, 5, 9, 0, 0, 0, DateTimeKind.Unspecified), + DoctorId = 3, + IsRepeat = false, + PatientId = 8, + RoomNumber = "303" + }, + new + { + Id = 10, + AppointmentDateTime = new DateTime(2026, 2, 8, 9, 0, 0, 0, DateTimeKind.Unspecified), + DoctorId = 2, + IsRepeat = false, + PatientId = 5, + RoomNumber = "102" + }, + new + { + Id = 11, + AppointmentDateTime = new DateTime(2026, 2, 22, 11, 30, 0, 0, DateTimeKind.Unspecified), + DoctorId = 6, + IsRepeat = false, + PatientId = 5, + RoomNumber = "606" + }); + }); + + modelBuilder.Entity("Polyclinic.Domain.Entities.Doctor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BirthDate") + .HasColumnType("datetime2"); + + b.Property("ExperienceYears") + .HasColumnType("int"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("PassportNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("SpecializationId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SpecializationId"); + + b.ToTable("Doctors"); + + b.HasData( + new + { + Id = 1, + BirthDate = new DateTime(1980, 5, 15, 0, 0, 0, 0, DateTimeKind.Unspecified), + ExperienceYears = 15, + FullName = "Иванов Иван Иванович", + PassportNumber = "4501 123456", + SpecializationId = 1 + }, + new + { + Id = 2, + BirthDate = new DateTime(1975, 8, 22, 0, 0, 0, 0, DateTimeKind.Unspecified), + ExperienceYears = 20, + FullName = "Петров Петр Петрович", + PassportNumber = "4502 234567", + SpecializationId = 2 + }, + new + { + Id = 3, + BirthDate = new DateTime(1985, 3, 10, 0, 0, 0, 0, DateTimeKind.Unspecified), + ExperienceYears = 12, + FullName = "Сидорова Анна Сергеевна", + PassportNumber = "4503 345678", + SpecializationId = 3 + }, + new + { + Id = 4, + BirthDate = new DateTime(1990, 11, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), + ExperienceYears = 8, + FullName = "Козлов Дмитрий Николаевич", + PassportNumber = "4504 456789", + SpecializationId = 4 + }, + new + { + Id = 5, + BirthDate = new DateTime(1982, 7, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + ExperienceYears = 14, + FullName = "Морозова Елена Владимировна", + PassportNumber = "4505 567890", + SpecializationId = 5 + }, + new + { + Id = 6, + BirthDate = new DateTime(1978, 9, 25, 0, 0, 0, 0, DateTimeKind.Unspecified), + ExperienceYears = 18, + FullName = "Волков Андрей Игоревич", + PassportNumber = "4506 678901", + SpecializationId = 6 + }, + new + { + Id = 7, + BirthDate = new DateTime(1988, 2, 14, 0, 0, 0, 0, DateTimeKind.Unspecified), + ExperienceYears = 10, + FullName = "Соколова Татьяна Александровна", + PassportNumber = "4507 789012", + SpecializationId = 7 + }, + new + { + Id = 8, + BirthDate = new DateTime(1992, 6, 5, 0, 0, 0, 0, DateTimeKind.Unspecified), + ExperienceYears = 6, + FullName = "Лебедев Михаил Сергеевич", + PassportNumber = "4508 890123", + SpecializationId = 8 + }, + new + { + Id = 9, + BirthDate = new DateTime(1983, 12, 3, 0, 0, 0, 0, DateTimeKind.Unspecified), + ExperienceYears = 13, + FullName = "Николаева Ольга Викторовна", + PassportNumber = "4509 901234", + SpecializationId = 9 + }, + new + { + Id = 10, + BirthDate = new DateTime(1970, 4, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + ExperienceYears = 25, + FullName = "Федоров Алексей Павлович", + PassportNumber = "4510 012345", + SpecializationId = 10 + }); + }); + + modelBuilder.Entity("Polyclinic.Domain.Entities.Patient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("nvarchar(250)"); + + b.Property("BirthDate") + .HasColumnType("datetime2"); + + b.Property("BloodGroup") + .HasColumnType("int"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("Gender") + .HasColumnType("int"); + + b.Property("PassportNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("PhoneNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("RhFactor") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Patients"); + + b.HasData( + new + { + Id = 1, + Address = "ул. Ленина, д. 10, кв. 25", + BirthDate = new DateTime(1990, 5, 15, 0, 0, 0, 0, DateTimeKind.Unspecified), + BloodGroup = 1, + FullName = "Смирнов Алексей Викторович", + Gender = 1, + PassportNumber = "6001 123456", + PhoneNumber = "+7 (999) 123-45-67", + RhFactor = 0 + }, + new + { + Id = 2, + Address = "ул. Гагарина, д. 5, кв. 12", + BirthDate = new DateTime(1985, 8, 22, 0, 0, 0, 0, DateTimeKind.Unspecified), + BloodGroup = 0, + FullName = "Кузнецова Елена Дмитриевна", + Gender = 2, + PassportNumber = "6002 234567", + PhoneNumber = "+7 (999) 234-56-78", + RhFactor = 0 + }, + new + { + Id = 3, + Address = "пр. Мира, д. 15, кв. 7", + BirthDate = new DateTime(1978, 3, 10, 0, 0, 0, 0, DateTimeKind.Unspecified), + BloodGroup = 2, + FullName = "Попов Сергей Иванович", + Gender = 1, + PassportNumber = "6003 345678", + PhoneNumber = "+7 (999) 345-67-89", + RhFactor = 1 + }, + new + { + Id = 4, + Address = "ул. Советская, д. 8, кв. 42", + BirthDate = new DateTime(1995, 11, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), + BloodGroup = 3, + FullName = "Васильева Мария Петровна", + Gender = 2, + PassportNumber = "6004 456789", + PhoneNumber = "+7 (999) 456-78-90", + RhFactor = 0 + }, + new + { + Id = 5, + Address = "ул. Пушкина, д. 3, кв. 56", + BirthDate = new DateTime(1982, 7, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + BloodGroup = 1, + FullName = "Соколов Андрей Николаевич", + Gender = 1, + PassportNumber = "6005 567890", + PhoneNumber = "+7 (999) 567-89-01", + RhFactor = 1 + }, + new + { + Id = 6, + Address = "пр. Ленинградский, д. 22, кв. 15", + BirthDate = new DateTime(1975, 9, 25, 0, 0, 0, 0, DateTimeKind.Unspecified), + BloodGroup = 0, + FullName = "Михайлова Анна Сергеевна", + Gender = 2, + PassportNumber = "6006 678901", + PhoneNumber = "+7 (999) 678-90-12", + RhFactor = 0 + }, + new + { + Id = 7, + Address = "ул. Кирова, д. 12, кв. 8", + BirthDate = new DateTime(1988, 2, 14, 0, 0, 0, 0, DateTimeKind.Unspecified), + BloodGroup = 2, + FullName = "Новиков Денис Александрович", + Gender = 1, + PassportNumber = "6007 789012", + PhoneNumber = "+7 (999) 789-01-23", + RhFactor = 0 + }, + new + { + Id = 8, + Address = "ул. Садовая, д. 7, кв. 31", + BirthDate = new DateTime(1992, 6, 5, 0, 0, 0, 0, DateTimeKind.Unspecified), + BloodGroup = 3, + FullName = "Морозова Татьяна Владимировна", + Gender = 2, + PassportNumber = "6008 890123", + PhoneNumber = "+7 (999) 890-12-34", + RhFactor = 1 + }, + new + { + Id = 9, + Address = "пр. Невский, д. 45, кв. 19", + BirthDate = new DateTime(1970, 12, 3, 0, 0, 0, 0, DateTimeKind.Unspecified), + BloodGroup = 1, + FullName = "Зайцев Игорь Павлович", + Gender = 1, + PassportNumber = "6009 901234", + PhoneNumber = "+7 (999) 901-23-45", + RhFactor = 0 + }, + new + { + Id = 10, + Address = "ул. Комсомольская, д. 6, кв. 23", + BirthDate = new DateTime(1965, 4, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + BloodGroup = 0, + FullName = "Волкова Ольга Игоревна", + Gender = 2, + PassportNumber = "6010 012345", + PhoneNumber = "+7 (999) 012-34-56", + RhFactor = 0 + }, + new + { + Id = 11, + Address = "ул. Мичурина, д. 18, кв. 67", + BirthDate = new DateTime(1998, 1, 8, 0, 0, 0, 0, DateTimeKind.Unspecified), + BloodGroup = 2, + FullName = "Белова Наталья Сергеевна", + Gender = 2, + PassportNumber = "6011 123456", + PhoneNumber = "+7 (999) 123-56-78", + RhFactor = 0 + }, + new + { + Id = 12, + Address = "ул. Лермонтова, д. 9, кв. 14", + BirthDate = new DateTime(1983, 9, 12, 0, 0, 0, 0, DateTimeKind.Unspecified), + BloodGroup = 3, + FullName = "Карпов Евгений Владимирович", + Gender = 1, + PassportNumber = "6012 234567", + PhoneNumber = "+7 (999) 234-67-89", + RhFactor = 1 + }); + }); + + modelBuilder.Entity("Polyclinic.Domain.Entities.Specialization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.ToTable("Specializations"); + + b.HasData( + new + { + Id = 1, + Code = "THERAPIST", + Description = "Врач общей практики", + Name = "Терапевт" + }, + new + { + Id = 2, + Code = "SURGEON", + Description = "Проведение операций", + Name = "Хирург" + }, + new + { + Id = 3, + Code = "CARDIOLOGIST", + Description = "Заболевания сердца", + Name = "Кардиолог" + }, + new + { + Id = 4, + Code = "NEUROLOGIST", + Description = "Заболевания нервной системы", + Name = "Невролог" + }, + new + { + Id = 5, + Code = "PEDIATRICIAN", + Description = "Детские болезни", + Name = "Педиатр" + }, + new + { + Id = 6, + Code = "GYNECOLOGIST", + Description = "Женское здоровье", + Name = "Гинеколог" + }, + new + { + Id = 7, + Code = "OPHTHALMOLOGIST", + Description = "Заболевания глаз", + Name = "Офтальмолог" + }, + new + { + Id = 8, + Code = "ENT", + Description = "Ухо, горло, нос", + Name = "Отоларинголог" + }, + new + { + Id = 9, + Code = "DERMATOLOGIST", + Description = "Кожные заболевания", + Name = "Дерматолог" + }, + new + { + Id = 10, + Code = "ENDOCRINOLOGIST", + Description = "Гормональные нарушения", + Name = "Эндокринолог" + }); + }); + + modelBuilder.Entity("Polyclinic.Domain.Entities.Appointment", b => + { + b.HasOne("Polyclinic.Domain.Entities.Doctor", "Doctor") + .WithMany("Appointments") + .HasForeignKey("DoctorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Polyclinic.Domain.Entities.Patient", "Patient") + .WithMany("Appointments") + .HasForeignKey("PatientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Doctor"); + + b.Navigation("Patient"); + }); + + modelBuilder.Entity("Polyclinic.Domain.Entities.Doctor", b => + { + b.HasOne("Polyclinic.Domain.Entities.Specialization", "Specialization") + .WithMany("Doctors") + .HasForeignKey("SpecializationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Specialization"); + }); + + modelBuilder.Entity("Polyclinic.Domain.Entities.Doctor", b => + { + b.Navigation("Appointments"); + }); + + modelBuilder.Entity("Polyclinic.Domain.Entities.Patient", b => + { + b.Navigation("Appointments"); + }); + + modelBuilder.Entity("Polyclinic.Domain.Entities.Specialization", b => + { + b.Navigation("Doctors"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Polyclinic/Polyclinic.Infrastructure.EfCore/Migrations/20260216193102_InitialCreate.cs b/Polyclinic/Polyclinic.Infrastructure.EfCore/Migrations/20260216193102_InitialCreate.cs new file mode 100644 index 000000000..e36d11bca --- /dev/null +++ b/Polyclinic/Polyclinic.Infrastructure.EfCore/Migrations/20260216193102_InitialCreate.cs @@ -0,0 +1,206 @@ +// +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace Polyclinic.Infrastructure.EfCore.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Patients", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + PassportNumber = table.Column(type: "nvarchar(20)", maxLength: 20, nullable: false), + FullName = table.Column(type: "nvarchar(150)", maxLength: 150, nullable: false), + Gender = table.Column(type: "int", nullable: false), + BirthDate = table.Column(type: "datetime2", nullable: false), + Address = table.Column(type: "nvarchar(250)", maxLength: 250, nullable: false), + BloodGroup = table.Column(type: "int", nullable: false), + RhFactor = table.Column(type: "int", nullable: false), + PhoneNumber = table.Column(type: "nvarchar(20)", maxLength: 20, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Patients", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Specializations", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + Description = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: false), + Code = table.Column(type: "nvarchar(20)", maxLength: 20, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Specializations", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Doctors", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + PassportNumber = table.Column(type: "nvarchar(20)", maxLength: 20, nullable: false), + FullName = table.Column(type: "nvarchar(150)", maxLength: 150, nullable: false), + BirthDate = table.Column(type: "datetime2", nullable: false), + SpecializationId = table.Column(type: "int", nullable: false), + ExperienceYears = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Doctors", x => x.Id); + table.ForeignKey( + name: "FK_Doctors_Specializations_SpecializationId", + column: x => x.SpecializationId, + principalTable: "Specializations", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Appointments", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + AppointmentDateTime = table.Column(type: "datetime2", nullable: false), + RoomNumber = table.Column(type: "nvarchar(10)", maxLength: 10, nullable: false), + IsRepeat = table.Column(type: "bit", nullable: false), + PatientId = table.Column(type: "int", nullable: false), + DoctorId = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Appointments", x => x.Id); + table.ForeignKey( + name: "FK_Appointments_Doctors_DoctorId", + column: x => x.DoctorId, + principalTable: "Doctors", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Appointments_Patients_PatientId", + column: x => x.PatientId, + principalTable: "Patients", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.InsertData( + table: "Patients", + columns: new[] { "Id", "Address", "BirthDate", "BloodGroup", "FullName", "Gender", "PassportNumber", "PhoneNumber", "RhFactor" }, + values: new object[,] + { + { 1, "ул. Ленина, д. 10, кв. 25", new DateTime(1990, 5, 15, 0, 0, 0, 0, DateTimeKind.Unspecified), 1, "Смирнов Алексей Викторович", 1, "6001 123456", "+7 (999) 123-45-67", 0 }, + { 2, "ул. Гагарина, д. 5, кв. 12", new DateTime(1985, 8, 22, 0, 0, 0, 0, DateTimeKind.Unspecified), 0, "Кузнецова Елена Дмитриевна", 2, "6002 234567", "+7 (999) 234-56-78", 0 }, + { 3, "пр. Мира, д. 15, кв. 7", new DateTime(1978, 3, 10, 0, 0, 0, 0, DateTimeKind.Unspecified), 2, "Попов Сергей Иванович", 1, "6003 345678", "+7 (999) 345-67-89", 1 }, + { 4, "ул. Советская, д. 8, кв. 42", new DateTime(1995, 11, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), 3, "Васильева Мария Петровна", 2, "6004 456789", "+7 (999) 456-78-90", 0 }, + { 5, "ул. Пушкина, д. 3, кв. 56", new DateTime(1982, 7, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), 1, "Соколов Андрей Николаевич", 1, "6005 567890", "+7 (999) 567-89-01", 1 }, + { 6, "пр. Ленинградский, д. 22, кв. 15", new DateTime(1975, 9, 25, 0, 0, 0, 0, DateTimeKind.Unspecified), 0, "Михайлова Анна Сергеевна", 2, "6006 678901", "+7 (999) 678-90-12", 0 }, + { 7, "ул. Кирова, д. 12, кв. 8", new DateTime(1988, 2, 14, 0, 0, 0, 0, DateTimeKind.Unspecified), 2, "Новиков Денис Александрович", 1, "6007 789012", "+7 (999) 789-01-23", 0 }, + { 8, "ул. Садовая, д. 7, кв. 31", new DateTime(1992, 6, 5, 0, 0, 0, 0, DateTimeKind.Unspecified), 3, "Морозова Татьяна Владимировна", 2, "6008 890123", "+7 (999) 890-12-34", 1 }, + { 9, "пр. Невский, д. 45, кв. 19", new DateTime(1970, 12, 3, 0, 0, 0, 0, DateTimeKind.Unspecified), 1, "Зайцев Игорь Павлович", 1, "6009 901234", "+7 (999) 901-23-45", 0 }, + { 10, "ул. Комсомольская, д. 6, кв. 23", new DateTime(1965, 4, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), 0, "Волкова Ольга Игоревна", 2, "6010 012345", "+7 (999) 012-34-56", 0 }, + { 11, "ул. Мичурина, д. 18, кв. 67", new DateTime(1998, 1, 8, 0, 0, 0, 0, DateTimeKind.Unspecified), 2, "Белова Наталья Сергеевна", 2, "6011 123456", "+7 (999) 123-56-78", 0 }, + { 12, "ул. Лермонтова, д. 9, кв. 14", new DateTime(1983, 9, 12, 0, 0, 0, 0, DateTimeKind.Unspecified), 3, "Карпов Евгений Владимирович", 1, "6012 234567", "+7 (999) 234-67-89", 1 } + }); + + migrationBuilder.InsertData( + table: "Specializations", + columns: new[] { "Id", "Code", "Description", "Name" }, + values: new object[,] + { + { 1, "THERAPIST", "Врач общей практики", "Терапевт" }, + { 2, "SURGEON", "Проведение операций", "Хирург" }, + { 3, "CARDIOLOGIST", "Заболевания сердца", "Кардиолог" }, + { 4, "NEUROLOGIST", "Заболевания нервной системы", "Невролог" }, + { 5, "PEDIATRICIAN", "Детские болезни", "Педиатр" }, + { 6, "GYNECOLOGIST", "Женское здоровье", "Гинеколог" }, + { 7, "OPHTHALMOLOGIST", "Заболевания глаз", "Офтальмолог" }, + { 8, "ENT", "Ухо, горло, нос", "Отоларинголог" }, + { 9, "DERMATOLOGIST", "Кожные заболевания", "Дерматолог" }, + { 10, "ENDOCRINOLOGIST", "Гормональные нарушения", "Эндокринолог" } + }); + + migrationBuilder.InsertData( + table: "Doctors", + columns: new[] { "Id", "BirthDate", "ExperienceYears", "FullName", "PassportNumber", "SpecializationId" }, + values: new object[,] + { + { 1, new DateTime(1980, 5, 15, 0, 0, 0, 0, DateTimeKind.Unspecified), 15, "Иванов Иван Иванович", "4501 123456", 1 }, + { 2, new DateTime(1975, 8, 22, 0, 0, 0, 0, DateTimeKind.Unspecified), 20, "Петров Петр Петрович", "4502 234567", 2 }, + { 3, new DateTime(1985, 3, 10, 0, 0, 0, 0, DateTimeKind.Unspecified), 12, "Сидорова Анна Сергеевна", "4503 345678", 3 }, + { 4, new DateTime(1990, 11, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), 8, "Козлов Дмитрий Николаевич", "4504 456789", 4 }, + { 5, new DateTime(1982, 7, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), 14, "Морозова Елена Владимировна", "4505 567890", 5 }, + { 6, new DateTime(1978, 9, 25, 0, 0, 0, 0, DateTimeKind.Unspecified), 18, "Волков Андрей Игоревич", "4506 678901", 6 }, + { 7, new DateTime(1988, 2, 14, 0, 0, 0, 0, DateTimeKind.Unspecified), 10, "Соколова Татьяна Александровна", "4507 789012", 7 }, + { 8, new DateTime(1992, 6, 5, 0, 0, 0, 0, DateTimeKind.Unspecified), 6, "Лебедев Михаил Сергеевич", "4508 890123", 8 }, + { 9, new DateTime(1983, 12, 3, 0, 0, 0, 0, DateTimeKind.Unspecified), 13, "Николаева Ольга Викторовна", "4509 901234", 9 }, + { 10, new DateTime(1970, 4, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), 25, "Федоров Алексей Павлович", "4510 012345", 10 } + }); + + migrationBuilder.InsertData( + table: "Appointments", + columns: new[] { "Id", "AppointmentDateTime", "DoctorId", "IsRepeat", "PatientId", "RoomNumber" }, + values: new object[,] + { + { 1, new DateTime(2026, 2, 5, 10, 0, 0, 0, DateTimeKind.Unspecified), 1, false, 1, "101" }, + { 2, new DateTime(2026, 2, 5, 11, 0, 0, 0, DateTimeKind.Unspecified), 1, true, 2, "101" }, + { 3, new DateTime(2026, 2, 10, 14, 30, 0, 0, DateTimeKind.Unspecified), 2, false, 3, "202" }, + { 4, new DateTime(2026, 2, 15, 9, 15, 0, 0, DateTimeKind.Unspecified), 3, false, 4, "303" }, + { 5, new DateTime(2026, 2, 12, 13, 30, 0, 0, DateTimeKind.Unspecified), 1, false, 8, "101" }, + { 6, new DateTime(2026, 2, 16, 9, 30, 0, 0, DateTimeKind.Unspecified), 1, false, 11, "101" }, + { 7, new DateTime(2026, 1, 15, 10, 0, 0, 0, DateTimeKind.Unspecified), 2, true, 6, "101" }, + { 8, new DateTime(2026, 1, 20, 11, 0, 0, 0, DateTimeKind.Unspecified), 2, true, 7, "202" }, + { 9, new DateTime(2026, 1, 5, 9, 0, 0, 0, DateTimeKind.Unspecified), 3, false, 8, "303" }, + { 10, new DateTime(2026, 2, 8, 9, 0, 0, 0, DateTimeKind.Unspecified), 2, false, 5, "102" }, + { 11, new DateTime(2026, 2, 22, 11, 30, 0, 0, DateTimeKind.Unspecified), 6, false, 5, "606" } + }); + + migrationBuilder.CreateIndex( + name: "IX_Appointments_DoctorId", + table: "Appointments", + column: "DoctorId"); + + migrationBuilder.CreateIndex( + name: "IX_Appointments_PatientId", + table: "Appointments", + column: "PatientId"); + + migrationBuilder.CreateIndex( + name: "IX_Doctors_SpecializationId", + table: "Doctors", + column: "SpecializationId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Appointments"); + + migrationBuilder.DropTable( + name: "Doctors"); + + migrationBuilder.DropTable( + name: "Patients"); + + migrationBuilder.DropTable( + name: "Specializations"); + } + } +} diff --git a/Polyclinic/Polyclinic.Infrastructure.EfCore/Migrations/PolyclinicDbContextModelSnapshot.cs b/Polyclinic/Polyclinic.Infrastructure.EfCore/Migrations/PolyclinicDbContextModelSnapshot.cs new file mode 100644 index 000000000..3b052e867 --- /dev/null +++ b/Polyclinic/Polyclinic.Infrastructure.EfCore/Migrations/PolyclinicDbContextModelSnapshot.cs @@ -0,0 +1,624 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Polyclinic.Infrastructure.EfCore; + +#nullable disable + +namespace Polyclinic.Infrastructure.EfCore.Migrations +{ + [DbContext(typeof(PolyclinicDbContext))] + partial class PolyclinicDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.13") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Polyclinic.Domain.Entities.Appointment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AppointmentDateTime") + .HasColumnType("datetime2"); + + b.Property("DoctorId") + .HasColumnType("int"); + + b.Property("IsRepeat") + .HasColumnType("bit"); + + b.Property("PatientId") + .HasColumnType("int"); + + b.Property("RoomNumber") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.HasKey("Id"); + + b.HasIndex("DoctorId"); + + b.HasIndex("PatientId"); + + b.ToTable("Appointments"); + + b.HasData( + new + { + Id = 1, + AppointmentDateTime = new DateTime(2026, 2, 5, 10, 0, 0, 0, DateTimeKind.Unspecified), + DoctorId = 1, + IsRepeat = false, + PatientId = 1, + RoomNumber = "101" + }, + new + { + Id = 2, + AppointmentDateTime = new DateTime(2026, 2, 5, 11, 0, 0, 0, DateTimeKind.Unspecified), + DoctorId = 1, + IsRepeat = true, + PatientId = 2, + RoomNumber = "101" + }, + new + { + Id = 3, + AppointmentDateTime = new DateTime(2026, 2, 10, 14, 30, 0, 0, DateTimeKind.Unspecified), + DoctorId = 2, + IsRepeat = false, + PatientId = 3, + RoomNumber = "202" + }, + new + { + Id = 4, + AppointmentDateTime = new DateTime(2026, 2, 15, 9, 15, 0, 0, DateTimeKind.Unspecified), + DoctorId = 3, + IsRepeat = false, + PatientId = 4, + RoomNumber = "303" + }, + new + { + Id = 5, + AppointmentDateTime = new DateTime(2026, 2, 12, 13, 30, 0, 0, DateTimeKind.Unspecified), + DoctorId = 1, + IsRepeat = false, + PatientId = 8, + RoomNumber = "101" + }, + new + { + Id = 6, + AppointmentDateTime = new DateTime(2026, 2, 16, 9, 30, 0, 0, DateTimeKind.Unspecified), + DoctorId = 1, + IsRepeat = false, + PatientId = 11, + RoomNumber = "101" + }, + new + { + Id = 7, + AppointmentDateTime = new DateTime(2026, 1, 15, 10, 0, 0, 0, DateTimeKind.Unspecified), + DoctorId = 2, + IsRepeat = true, + PatientId = 6, + RoomNumber = "101" + }, + new + { + Id = 8, + AppointmentDateTime = new DateTime(2026, 1, 20, 11, 0, 0, 0, DateTimeKind.Unspecified), + DoctorId = 2, + IsRepeat = true, + PatientId = 7, + RoomNumber = "202" + }, + new + { + Id = 9, + AppointmentDateTime = new DateTime(2026, 1, 5, 9, 0, 0, 0, DateTimeKind.Unspecified), + DoctorId = 3, + IsRepeat = false, + PatientId = 8, + RoomNumber = "303" + }, + new + { + Id = 10, + AppointmentDateTime = new DateTime(2026, 2, 8, 9, 0, 0, 0, DateTimeKind.Unspecified), + DoctorId = 2, + IsRepeat = false, + PatientId = 5, + RoomNumber = "102" + }, + new + { + Id = 11, + AppointmentDateTime = new DateTime(2026, 2, 22, 11, 30, 0, 0, DateTimeKind.Unspecified), + DoctorId = 6, + IsRepeat = false, + PatientId = 5, + RoomNumber = "606" + }); + }); + + modelBuilder.Entity("Polyclinic.Domain.Entities.Doctor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BirthDate") + .HasColumnType("datetime2"); + + b.Property("ExperienceYears") + .HasColumnType("int"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("PassportNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("SpecializationId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SpecializationId"); + + b.ToTable("Doctors"); + + b.HasData( + new + { + Id = 1, + BirthDate = new DateTime(1980, 5, 15, 0, 0, 0, 0, DateTimeKind.Unspecified), + ExperienceYears = 15, + FullName = "Иванов Иван Иванович", + PassportNumber = "4501 123456", + SpecializationId = 1 + }, + new + { + Id = 2, + BirthDate = new DateTime(1975, 8, 22, 0, 0, 0, 0, DateTimeKind.Unspecified), + ExperienceYears = 20, + FullName = "Петров Петр Петрович", + PassportNumber = "4502 234567", + SpecializationId = 2 + }, + new + { + Id = 3, + BirthDate = new DateTime(1985, 3, 10, 0, 0, 0, 0, DateTimeKind.Unspecified), + ExperienceYears = 12, + FullName = "Сидорова Анна Сергеевна", + PassportNumber = "4503 345678", + SpecializationId = 3 + }, + new + { + Id = 4, + BirthDate = new DateTime(1990, 11, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), + ExperienceYears = 8, + FullName = "Козлов Дмитрий Николаевич", + PassportNumber = "4504 456789", + SpecializationId = 4 + }, + new + { + Id = 5, + BirthDate = new DateTime(1982, 7, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + ExperienceYears = 14, + FullName = "Морозова Елена Владимировна", + PassportNumber = "4505 567890", + SpecializationId = 5 + }, + new + { + Id = 6, + BirthDate = new DateTime(1978, 9, 25, 0, 0, 0, 0, DateTimeKind.Unspecified), + ExperienceYears = 18, + FullName = "Волков Андрей Игоревич", + PassportNumber = "4506 678901", + SpecializationId = 6 + }, + new + { + Id = 7, + BirthDate = new DateTime(1988, 2, 14, 0, 0, 0, 0, DateTimeKind.Unspecified), + ExperienceYears = 10, + FullName = "Соколова Татьяна Александровна", + PassportNumber = "4507 789012", + SpecializationId = 7 + }, + new + { + Id = 8, + BirthDate = new DateTime(1992, 6, 5, 0, 0, 0, 0, DateTimeKind.Unspecified), + ExperienceYears = 6, + FullName = "Лебедев Михаил Сергеевич", + PassportNumber = "4508 890123", + SpecializationId = 8 + }, + new + { + Id = 9, + BirthDate = new DateTime(1983, 12, 3, 0, 0, 0, 0, DateTimeKind.Unspecified), + ExperienceYears = 13, + FullName = "Николаева Ольга Викторовна", + PassportNumber = "4509 901234", + SpecializationId = 9 + }, + new + { + Id = 10, + BirthDate = new DateTime(1970, 4, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + ExperienceYears = 25, + FullName = "Федоров Алексей Павлович", + PassportNumber = "4510 012345", + SpecializationId = 10 + }); + }); + + modelBuilder.Entity("Polyclinic.Domain.Entities.Patient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("nvarchar(250)"); + + b.Property("BirthDate") + .HasColumnType("datetime2"); + + b.Property("BloodGroup") + .HasColumnType("int"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("Gender") + .HasColumnType("int"); + + b.Property("PassportNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("PhoneNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("RhFactor") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Patients"); + + b.HasData( + new + { + Id = 1, + Address = "ул. Ленина, д. 10, кв. 25", + BirthDate = new DateTime(1990, 5, 15, 0, 0, 0, 0, DateTimeKind.Unspecified), + BloodGroup = 1, + FullName = "Смирнов Алексей Викторович", + Gender = 1, + PassportNumber = "6001 123456", + PhoneNumber = "+7 (999) 123-45-67", + RhFactor = 0 + }, + new + { + Id = 2, + Address = "ул. Гагарина, д. 5, кв. 12", + BirthDate = new DateTime(1985, 8, 22, 0, 0, 0, 0, DateTimeKind.Unspecified), + BloodGroup = 0, + FullName = "Кузнецова Елена Дмитриевна", + Gender = 2, + PassportNumber = "6002 234567", + PhoneNumber = "+7 (999) 234-56-78", + RhFactor = 0 + }, + new + { + Id = 3, + Address = "пр. Мира, д. 15, кв. 7", + BirthDate = new DateTime(1978, 3, 10, 0, 0, 0, 0, DateTimeKind.Unspecified), + BloodGroup = 2, + FullName = "Попов Сергей Иванович", + Gender = 1, + PassportNumber = "6003 345678", + PhoneNumber = "+7 (999) 345-67-89", + RhFactor = 1 + }, + new + { + Id = 4, + Address = "ул. Советская, д. 8, кв. 42", + BirthDate = new DateTime(1995, 11, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), + BloodGroup = 3, + FullName = "Васильева Мария Петровна", + Gender = 2, + PassportNumber = "6004 456789", + PhoneNumber = "+7 (999) 456-78-90", + RhFactor = 0 + }, + new + { + Id = 5, + Address = "ул. Пушкина, д. 3, кв. 56", + BirthDate = new DateTime(1982, 7, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + BloodGroup = 1, + FullName = "Соколов Андрей Николаевич", + Gender = 1, + PassportNumber = "6005 567890", + PhoneNumber = "+7 (999) 567-89-01", + RhFactor = 1 + }, + new + { + Id = 6, + Address = "пр. Ленинградский, д. 22, кв. 15", + BirthDate = new DateTime(1975, 9, 25, 0, 0, 0, 0, DateTimeKind.Unspecified), + BloodGroup = 0, + FullName = "Михайлова Анна Сергеевна", + Gender = 2, + PassportNumber = "6006 678901", + PhoneNumber = "+7 (999) 678-90-12", + RhFactor = 0 + }, + new + { + Id = 7, + Address = "ул. Кирова, д. 12, кв. 8", + BirthDate = new DateTime(1988, 2, 14, 0, 0, 0, 0, DateTimeKind.Unspecified), + BloodGroup = 2, + FullName = "Новиков Денис Александрович", + Gender = 1, + PassportNumber = "6007 789012", + PhoneNumber = "+7 (999) 789-01-23", + RhFactor = 0 + }, + new + { + Id = 8, + Address = "ул. Садовая, д. 7, кв. 31", + BirthDate = new DateTime(1992, 6, 5, 0, 0, 0, 0, DateTimeKind.Unspecified), + BloodGroup = 3, + FullName = "Морозова Татьяна Владимировна", + Gender = 2, + PassportNumber = "6008 890123", + PhoneNumber = "+7 (999) 890-12-34", + RhFactor = 1 + }, + new + { + Id = 9, + Address = "пр. Невский, д. 45, кв. 19", + BirthDate = new DateTime(1970, 12, 3, 0, 0, 0, 0, DateTimeKind.Unspecified), + BloodGroup = 1, + FullName = "Зайцев Игорь Павлович", + Gender = 1, + PassportNumber = "6009 901234", + PhoneNumber = "+7 (999) 901-23-45", + RhFactor = 0 + }, + new + { + Id = 10, + Address = "ул. Комсомольская, д. 6, кв. 23", + BirthDate = new DateTime(1965, 4, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + BloodGroup = 0, + FullName = "Волкова Ольга Игоревна", + Gender = 2, + PassportNumber = "6010 012345", + PhoneNumber = "+7 (999) 012-34-56", + RhFactor = 0 + }, + new + { + Id = 11, + Address = "ул. Мичурина, д. 18, кв. 67", + BirthDate = new DateTime(1998, 1, 8, 0, 0, 0, 0, DateTimeKind.Unspecified), + BloodGroup = 2, + FullName = "Белова Наталья Сергеевна", + Gender = 2, + PassportNumber = "6011 123456", + PhoneNumber = "+7 (999) 123-56-78", + RhFactor = 0 + }, + new + { + Id = 12, + Address = "ул. Лермонтова, д. 9, кв. 14", + BirthDate = new DateTime(1983, 9, 12, 0, 0, 0, 0, DateTimeKind.Unspecified), + BloodGroup = 3, + FullName = "Карпов Евгений Владимирович", + Gender = 1, + PassportNumber = "6012 234567", + PhoneNumber = "+7 (999) 234-67-89", + RhFactor = 1 + }); + }); + + modelBuilder.Entity("Polyclinic.Domain.Entities.Specialization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.ToTable("Specializations"); + + b.HasData( + new + { + Id = 1, + Code = "THERAPIST", + Description = "Врач общей практики", + Name = "Терапевт" + }, + new + { + Id = 2, + Code = "SURGEON", + Description = "Проведение операций", + Name = "Хирург" + }, + new + { + Id = 3, + Code = "CARDIOLOGIST", + Description = "Заболевания сердца", + Name = "Кардиолог" + }, + new + { + Id = 4, + Code = "NEUROLOGIST", + Description = "Заболевания нервной системы", + Name = "Невролог" + }, + new + { + Id = 5, + Code = "PEDIATRICIAN", + Description = "Детские болезни", + Name = "Педиатр" + }, + new + { + Id = 6, + Code = "GYNECOLOGIST", + Description = "Женское здоровье", + Name = "Гинеколог" + }, + new + { + Id = 7, + Code = "OPHTHALMOLOGIST", + Description = "Заболевания глаз", + Name = "Офтальмолог" + }, + new + { + Id = 8, + Code = "ENT", + Description = "Ухо, горло, нос", + Name = "Отоларинголог" + }, + new + { + Id = 9, + Code = "DERMATOLOGIST", + Description = "Кожные заболевания", + Name = "Дерматолог" + }, + new + { + Id = 10, + Code = "ENDOCRINOLOGIST", + Description = "Гормональные нарушения", + Name = "Эндокринолог" + }); + }); + + modelBuilder.Entity("Polyclinic.Domain.Entities.Appointment", b => + { + b.HasOne("Polyclinic.Domain.Entities.Doctor", "Doctor") + .WithMany("Appointments") + .HasForeignKey("DoctorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Polyclinic.Domain.Entities.Patient", "Patient") + .WithMany("Appointments") + .HasForeignKey("PatientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Doctor"); + + b.Navigation("Patient"); + }); + + modelBuilder.Entity("Polyclinic.Domain.Entities.Doctor", b => + { + b.HasOne("Polyclinic.Domain.Entities.Specialization", "Specialization") + .WithMany("Doctors") + .HasForeignKey("SpecializationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Specialization"); + }); + + modelBuilder.Entity("Polyclinic.Domain.Entities.Doctor", b => + { + b.Navigation("Appointments"); + }); + + modelBuilder.Entity("Polyclinic.Domain.Entities.Patient", b => + { + b.Navigation("Appointments"); + }); + + modelBuilder.Entity("Polyclinic.Domain.Entities.Specialization", b => + { + b.Navigation("Doctors"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Polyclinic/Polyclinic.Infrastructure.EfCore/Polyclinic.Infrastructure.EfCore.csproj b/Polyclinic/Polyclinic.Infrastructure.EfCore/Polyclinic.Infrastructure.EfCore.csproj new file mode 100644 index 000000000..edff78024 --- /dev/null +++ b/Polyclinic/Polyclinic.Infrastructure.EfCore/Polyclinic.Infrastructure.EfCore.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + diff --git a/Polyclinic/Polyclinic.Infrastructure.EfCore/PolyclinicDbContext.cs b/Polyclinic/Polyclinic.Infrastructure.EfCore/PolyclinicDbContext.cs new file mode 100644 index 000000000..6965e1b19 --- /dev/null +++ b/Polyclinic/Polyclinic.Infrastructure.EfCore/PolyclinicDbContext.cs @@ -0,0 +1,165 @@ +using Microsoft.EntityFrameworkCore; +using Polyclinic.Domain; +using Polyclinic.Domain.Entities; + +namespace Polyclinic.Infrastructure.EfCore; + +/// +/// Контекст базы данных поликлиники +/// +public class PolyclinicDbContext( + DbContextOptions options, + PolyclinicFixture fixture) : DbContext(options) +{ + /// + /// Таблица специализаций врачей (справочник) + /// + public DbSet Specializations { get; set; } + + /// + /// Таблица врачей поликлиники + /// + public DbSet Doctors { get; set; } + + /// + /// Таблица пациентов + /// + public DbSet Patients { get; set; } + + /// + /// Таблица записей на прием (журнал посещений) + /// + public DbSet Appointments { get; set; } + + /// + /// Настройка модели базы данных при её создании + /// + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + ConfigureSpecialization(modelBuilder); + ConfigureDoctor(modelBuilder); + ConfigurePatient(modelBuilder); + ConfigureAppointment(modelBuilder); + } + + /// + /// Конфигурация сущности + /// + private void ConfigureSpecialization(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + + entity.Property(e => e.Name) + .IsRequired() + .HasMaxLength(100); + + entity.Property(e => e.Code) + .IsRequired() + .HasMaxLength(20); + + entity.Property(e => e.Description) + .HasMaxLength(500); + + if (fixture.Specializations.Count > 0) + { + entity.HasData(fixture.Specializations); + } + }); + } + + /// + /// Конфигурация сущности + /// + private void ConfigureDoctor(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + + entity.Property(e => e.FullName) + .IsRequired() + .HasMaxLength(150); + + entity.Property(e => e.PassportNumber) + .IsRequired() + .HasMaxLength(20); + + entity.HasOne(d => d.Specialization) + .WithMany(s => s.Doctors) + .HasForeignKey(d => d.SpecializationId) + .OnDelete(DeleteBehavior.Restrict); + + if (fixture.Doctors.Count > 0) + { + entity.HasData(fixture.Doctors); + } + }); + } + + /// + /// Конфигурация сущности + /// + private void ConfigurePatient(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + + entity.Property(e => e.FullName) + .IsRequired() + .HasMaxLength(150); + + entity.Property(e => e.PassportNumber) + .IsRequired() + .HasMaxLength(20); + + entity.Property(e => e.Address) + .HasMaxLength(250); + + entity.Property(e => e.PhoneNumber) + .HasMaxLength(20); + + if (fixture.Patients.Count > 0) + { + entity.HasData(fixture.Patients); + } + }); + } + + /// + /// Конфигурация сущности + /// + private void ConfigureAppointment(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + + entity.Property(e => e.RoomNumber) + .HasMaxLength(10) + .IsRequired(); + + entity.Property(e => e.AppointmentDateTime) + .HasColumnType("datetime2"); + + entity.HasOne(a => a.Patient) + .WithMany(p => p.Appointments) + .HasForeignKey(a => a.PatientId) + .OnDelete(DeleteBehavior.Cascade); + + entity.HasOne(a => a.Doctor) + .WithMany(d => d.Appointments) + .HasForeignKey(a => a.DoctorId) + .OnDelete(DeleteBehavior.Restrict); + + if (fixture.Appointments.Count > 0) + { + entity.HasData(fixture.Appointments); + } + }); + } +} \ No newline at end of file diff --git a/Polyclinic/Polyclinic.Infrastructure.EfCore/Repositories/AppointmentRepository.cs b/Polyclinic/Polyclinic.Infrastructure.EfCore/Repositories/AppointmentRepository.cs new file mode 100644 index 000000000..2396d127b --- /dev/null +++ b/Polyclinic/Polyclinic.Infrastructure.EfCore/Repositories/AppointmentRepository.cs @@ -0,0 +1,71 @@ +using Microsoft.EntityFrameworkCore; +using Polyclinic.Domain; +using Polyclinic.Domain.Entities; + +namespace Polyclinic.Infrastructure.EfCore.Repositories; + +/// +/// Репозиторий для управления записями на прием +/// +public class AppointmentRepository(PolyclinicDbContext context) : IRepository +{ + /// + /// Создаёт новую запись на прием в базе данных + /// + public async Task Create(Appointment entity) + { + await context.Appointments.AddAsync(entity); + await context.SaveChangesAsync(); + return entity; + } + + /// + /// Получает запись по идентификатору с данными о враче и пациенте + /// + public async Task Read(int entityId) + { + return await context.Appointments + .Include(a => a.Patient) + .Include(a => a.Doctor) + .ThenInclude(d => d!.Specialization) + .FirstOrDefaultAsync(a => a.Id == entityId); + } + + /// + /// Получает список всех записей с данными о врачах и пациентах + /// + public async Task> ReadAll() + { + return await context.Appointments + .Include(a => a.Patient) + .Include(a => a.Doctor) + .ThenInclude(d => d!.Specialization) + .ToListAsync(); + } + + /// + /// Обновляет данные записи на прием + /// + public async Task Update(Appointment entity) + { + context.Appointments.Update(entity); + await context.SaveChangesAsync(); + return entity; + } + + /// + /// Удаляет запись на прием по идентификатору + /// + public async Task Delete(int entityId) + { + var entity = await context.Appointments.FirstOrDefaultAsync(e => e.Id == entityId); + if (entity == null) + { + return false; + } + + context.Appointments.Remove(entity); + await context.SaveChangesAsync(); + return true; + } +} \ No newline at end of file diff --git a/Polyclinic/Polyclinic.Infrastructure.EfCore/Repositories/DoctorRepository.cs b/Polyclinic/Polyclinic.Infrastructure.EfCore/Repositories/DoctorRepository.cs new file mode 100644 index 000000000..b51313ca6 --- /dev/null +++ b/Polyclinic/Polyclinic.Infrastructure.EfCore/Repositories/DoctorRepository.cs @@ -0,0 +1,67 @@ +using Microsoft.EntityFrameworkCore; +using Polyclinic.Domain; +using Polyclinic.Domain.Entities; + +namespace Polyclinic.Infrastructure.EfCore.Repositories; + +/// +/// Репозиторий для управления врачами +/// +public class DoctorRepository(PolyclinicDbContext context) : IRepository +{ + /// + /// Создаёт нового врача в базе данных + /// + public async Task Create(Doctor entity) + { + await context.Doctors.AddAsync(entity); + await context.SaveChangesAsync(); + return entity; + } + + /// + /// Получает врача по идентификатору вместе со специализацией + /// + public async Task Read(int entityId) + { + return await context.Doctors + .Include(d => d.Specialization) + .FirstOrDefaultAsync(d => d.Id == entityId); + } + + /// + /// Получает список всех врачей вместе с их специализациями + /// + public async Task> ReadAll() + { + return await context.Doctors + .Include(d => d.Specialization) + .ToListAsync(); + } + + /// + /// Обновляет данные врача + /// + public async Task Update(Doctor entity) + { + context.Doctors.Update(entity); + await context.SaveChangesAsync(); + return entity; + } + + /// + /// Удаляет врача по идентификатору + /// + public async Task Delete(int entityId) + { + var entity = await context.Doctors.FirstOrDefaultAsync(e => e.Id == entityId); + if (entity == null) + { + return false; + } + + context.Doctors.Remove(entity); + await context.SaveChangesAsync(); + return true; + } +} \ No newline at end of file diff --git a/Polyclinic/Polyclinic.Infrastructure.EfCore/Repositories/PatientRepository.cs b/Polyclinic/Polyclinic.Infrastructure.EfCore/Repositories/PatientRepository.cs new file mode 100644 index 000000000..e68717bac --- /dev/null +++ b/Polyclinic/Polyclinic.Infrastructure.EfCore/Repositories/PatientRepository.cs @@ -0,0 +1,63 @@ +using Microsoft.EntityFrameworkCore; +using Polyclinic.Domain; +using Polyclinic.Domain.Entities; + +namespace Polyclinic.Infrastructure.EfCore.Repositories; + +/// +/// Репозиторий для управления пациентами +/// +public class PatientRepository(PolyclinicDbContext context) : IRepository +{ + /// + /// Создаёт нового пациента в базе данных + /// + public async Task Create(Patient entity) + { + await context.Patients.AddAsync(entity); + await context.SaveChangesAsync(); + return entity; + } + + /// + /// Получает пациента по идентификатору + /// + public async Task Read(int entityId) + { + return await context.Patients.FirstOrDefaultAsync(e => e.Id == entityId); + } + + /// + /// Получает список всех пациентов + /// + public async Task> ReadAll() + { + return await context.Patients.ToListAsync(); + } + + /// + /// Обновляет данные пациента + /// + public async Task Update(Patient entity) + { + context.Patients.Update(entity); + await context.SaveChangesAsync(); + return entity; + } + + /// + /// Удаляет пациента по идентификатору + /// + public async Task Delete(int entityId) + { + var entity = await context.Patients.FirstOrDefaultAsync(e => e.Id == entityId); + if (entity == null) + { + return false; + } + + context.Patients.Remove(entity); + await context.SaveChangesAsync(); + return true; + } +} \ No newline at end of file diff --git a/Polyclinic/Polyclinic.Infrastructure.EfCore/Repositories/SpecializationRepository.cs b/Polyclinic/Polyclinic.Infrastructure.EfCore/Repositories/SpecializationRepository.cs new file mode 100644 index 000000000..a2559e2d5 --- /dev/null +++ b/Polyclinic/Polyclinic.Infrastructure.EfCore/Repositories/SpecializationRepository.cs @@ -0,0 +1,63 @@ +using Microsoft.EntityFrameworkCore; +using Polyclinic.Domain; +using Polyclinic.Domain.Entities; + +namespace Polyclinic.Infrastructure.EfCore.Repositories; + +/// +/// Репозиторий для управления специализациями +/// +public class SpecializationRepository(PolyclinicDbContext context) : IRepository +{ + /// + /// Создаёт новую специализацию в базе данных + /// + public async Task Create(Specialization entity) + { + await context.Specializations.AddAsync(entity); + await context.SaveChangesAsync(); + return entity; + } + + /// + /// Получает специализацию по идентификатору + /// + public async Task Read(int entityId) + { + return await context.Specializations.FirstOrDefaultAsync(e => e.Id == entityId); + } + + /// + /// Получает список всех специализаций + /// + public async Task> ReadAll() + { + return await context.Specializations.ToListAsync(); + } + + /// + /// Обновляет данные специализации + /// + public async Task Update(Specialization entity) + { + context.Specializations.Update(entity); + await context.SaveChangesAsync(); + return entity; + } + + /// + /// Удаляет специализацию по идентификатору + /// + public async Task Delete(int entityId) + { + var entity = await context.Specializations.FirstOrDefaultAsync(e => e.Id == entityId); + if (entity == null) + { + return false; + } + + context.Specializations.Remove(entity); + await context.SaveChangesAsync(); + return true; + } +} \ No newline at end of file diff --git a/Polyclinic/Polyclinic.ServiceDefaults/Extensions.cs b/Polyclinic/Polyclinic.ServiceDefaults/Extensions.cs new file mode 100644 index 000000000..16471f423 --- /dev/null +++ b/Polyclinic/Polyclinic.ServiceDefaults/Extensions.cs @@ -0,0 +1,127 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Polyclinic.ServiceDefaults; + +// Adds common Aspire services: service discovery, resilience, health checks, and OpenTelemetry. +// This project should be referenced by each service project in your solution. +// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults +public static class Extensions +{ + private const string HealthEndpointPath = "/health"; + private const string AlivenessEndpointPath = "/alive"; + + public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.ConfigureOpenTelemetry(); + + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + // Turn on resilience by default + http.AddStandardResilienceHandler(); + + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + // Uncomment the following to restrict the allowed schemes for service discovery. + // builder.Services.Configure(options => + // { + // options.AllowedSchemes = ["https"]; + // }); + + return builder; + } + + public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddSource(builder.Environment.ApplicationName) + .AddAspNetCoreInstrumentation(tracing => + // Exclude health check requests from tracing + tracing.Filter = context => + !context.Request.Path.StartsWithSegments(HealthEndpointPath) + && !context.Request.Path.StartsWithSegments(AlivenessEndpointPath) + ) + // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) + //.AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) + //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) + //{ + // builder.Services.AddOpenTelemetry() + // .UseAzureMonitor(); + //} + + return builder; + } + + public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Services.AddHealthChecks() + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) + { + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks(HealthEndpointPath); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + + return app; + } +} diff --git a/Polyclinic/Polyclinic.ServiceDefaults/Polyclinic.ServiceDefaults.csproj b/Polyclinic/Polyclinic.ServiceDefaults/Polyclinic.ServiceDefaults.csproj new file mode 100644 index 000000000..1b6e209a7 --- /dev/null +++ b/Polyclinic/Polyclinic.ServiceDefaults/Polyclinic.ServiceDefaults.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + + + + diff --git a/Polyclinic/Polyclinic.Tests/PolyclinicTests.cs b/Polyclinic/Polyclinic.Tests/PolyclinicTests.cs index d9c3a412e..a80553106 100644 --- a/Polyclinic/Polyclinic.Tests/PolyclinicTests.cs +++ b/Polyclinic/Polyclinic.Tests/PolyclinicTests.cs @@ -1,4 +1,6 @@ -namespace Polyclinic.Tests; +using Polyclinic.Domain; + +namespace Polyclinic.Tests; /// /// Тесты для поликлиники с использованием фикстуры diff --git a/Polyclinic/Polyclinic.sln b/Polyclinic/Polyclinic.sln index 28573c5ce..d2de01ee2 100644 --- a/Polyclinic/Polyclinic.sln +++ b/Polyclinic/Polyclinic.sln @@ -7,6 +7,18 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Polyclinic.Domain", "Polycl EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Polyclinic.Tests", "Polyclinic.Tests\Polyclinic.Tests.csproj", "{0C24BBBB-C432-4776-9968-5DAD9432AB5A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Polyclinic.Infrastructure.EfCore", "Polyclinic.Infrastructure.EfCore\Polyclinic.Infrastructure.EfCore.csproj", "{33F86AF2-9AF7-425B-BA91-9CCF0FB7BBA5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Polyclinic.Application.Contracts", "Polyclinic.Application.Contracts\Polyclinic.Application.Contracts.csproj", "{BA50FC27-A077-4902-B5D4-EE6E5485F0A3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Polyclinic.Application", "Polyclinic.Application\Polyclinic.Application.csproj", "{7DA00279-0DD6-4DE2-8790-E24B87AFA619}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Polyclinic.Api.Host", "Polyclinic.Api.Host\Polyclinic.Api.Host.csproj", "{D09E3941-0AB9-C8DB-7E7A-424732E05A20}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Polyclinic.AppHost", "Polyclinic.AppHost\Polyclinic.AppHost.csproj", "{472D09AB-BC31-4355-967F-DE9994323957}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Polyclinic.ServiceDefaults", "Polyclinic.ServiceDefaults\Polyclinic.ServiceDefaults.csproj", "{B3604DE1-E61C-254E-A2A2-7B3A153D5ED4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,6 +33,30 @@ Global {0C24BBBB-C432-4776-9968-5DAD9432AB5A}.Debug|Any CPU.Build.0 = Debug|Any CPU {0C24BBBB-C432-4776-9968-5DAD9432AB5A}.Release|Any CPU.ActiveCfg = Release|Any CPU {0C24BBBB-C432-4776-9968-5DAD9432AB5A}.Release|Any CPU.Build.0 = Release|Any CPU + {33F86AF2-9AF7-425B-BA91-9CCF0FB7BBA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {33F86AF2-9AF7-425B-BA91-9CCF0FB7BBA5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {33F86AF2-9AF7-425B-BA91-9CCF0FB7BBA5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {33F86AF2-9AF7-425B-BA91-9CCF0FB7BBA5}.Release|Any CPU.Build.0 = Release|Any CPU + {BA50FC27-A077-4902-B5D4-EE6E5485F0A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BA50FC27-A077-4902-B5D4-EE6E5485F0A3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BA50FC27-A077-4902-B5D4-EE6E5485F0A3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BA50FC27-A077-4902-B5D4-EE6E5485F0A3}.Release|Any CPU.Build.0 = Release|Any CPU + {7DA00279-0DD6-4DE2-8790-E24B87AFA619}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7DA00279-0DD6-4DE2-8790-E24B87AFA619}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7DA00279-0DD6-4DE2-8790-E24B87AFA619}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7DA00279-0DD6-4DE2-8790-E24B87AFA619}.Release|Any CPU.Build.0 = Release|Any CPU + {D09E3941-0AB9-C8DB-7E7A-424732E05A20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D09E3941-0AB9-C8DB-7E7A-424732E05A20}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D09E3941-0AB9-C8DB-7E7A-424732E05A20}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D09E3941-0AB9-C8DB-7E7A-424732E05A20}.Release|Any CPU.Build.0 = Release|Any CPU + {472D09AB-BC31-4355-967F-DE9994323957}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {472D09AB-BC31-4355-967F-DE9994323957}.Debug|Any CPU.Build.0 = Debug|Any CPU + {472D09AB-BC31-4355-967F-DE9994323957}.Release|Any CPU.ActiveCfg = Release|Any CPU + {472D09AB-BC31-4355-967F-DE9994323957}.Release|Any CPU.Build.0 = Release|Any CPU + {B3604DE1-E61C-254E-A2A2-7B3A153D5ED4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B3604DE1-E61C-254E-A2A2-7B3A153D5ED4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B3604DE1-E61C-254E-A2A2-7B3A153D5ED4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B3604DE1-E61C-254E-A2A2-7B3A153D5ED4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 939023f536cf191ebba527fdff57515292d89d54 Mon Sep 17 00:00:00 2001 From: Tattaboe Date: Tue, 17 Feb 2026 00:06:51 +0400 Subject: [PATCH 6/7] Update README.md --- README.md | 153 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 125 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 5b1298a8d..ce9dbf2c9 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,15 @@ -# Разработка корпоративных приложений. Лабораторная работа №1. +# Разработка корпоративных приложений. Лабораторные работы №1-3 -### Цель - Реализация объектной модели данных и unit-тестов +## Лабораторная работа №1 -Необходимо подготовить структуру классов, описывающих предметную область. В каждом из заданий присутствует часть, связанная с обработкой данных, представленная в разделе «Unit-тесты». Данную часть необходимо реализовать в виде unit-тестов: подготовить тестовые данные, выполнить запрос с использованием LINQ, проверить результаты. Хранение данных на этом этапе допускается осуществлять в памяти в виде коллекций. +### Цель +Реализация объектной модели данных и unit-тестов. -### Предметная область - Поликлиника +### Описание +Необходимо подготовить структуру классов, описывающих предметную область. В каждом из заданий присутствует часть, связанная с обработкой данных, представленная в разделе «Unit-тесты». Данную часть необходимо реализовать в виде unit-тестов: подготовить тестовые данные, выполнить запрос с использованием LINQ, проверить результаты. Хранение данных на этом этапе допускается осуществлять в памяти в виде коллекций. -В базе данных поликлиники содержится информация о записях пациентов на прием к врачам. +### Предметная область - Поликлиника +В базе данных поликлиники содержится информация о записях пациентов на прием к врачам. Пациент характеризуется: номером паспорта, ФИО, полом, датой рождения, адресом, группой крови, резус фактором и контактным телефоном. Пол пациента является перечислением. @@ -20,32 +23,126 @@ ### Юнит-тесты - - Вывести информацию о всех врачах, стаж работы которых не менее 10 лет. - - Вывести информацию о всех пациентах, записанных на прием к указанному врачу, упорядочить по ФИО. - - Вывести информацию о количестве повторных приемов пациентов за последний месяц. - - Вывести информацию о пациентах старше 30 лет, которые записаны на прием к нескольким врачам, упорядочить по дате рождения. - - Вывести информацию о приемах за текущий месяц, проходящих в выбранном кабинете. +- Вывести информацию о всех врачах, стаж работы которых не менее 10 лет. +- Вывести информацию о всех пациентах, записанных на прием к указанному врачу, упорядочить по ФИО. +- Вывести информацию о количестве повторных приемов пациентов за последний месяц. +- Вывести информацию о пациентах старше 30 лет, которые записаны на прием к нескольким врачам, упорядочить по дате рождения. +- Вывести информацию о приемах за текущий месяц, проходящих в выбранном кабинете. + +## Лабораторная работа №2 + +### Цель +Реализация серверного приложения с CRUD-операциями и аналитикой. + +### Реализация +Создано Web API на ASP.NET Core со следующими возможностями: + +**CRUD-контроллеры:** +- `PatientController` - управление пациентами +- `DoctorController` - управление врачами +- `SpecializationController` - управление специализациями +- `AppointmentController` - управление записями на прием + +**Аналитический контроллер (`AnalyticsController`):** +- `GET /api/analytics/doctors/experienced` - врачи со стажем ≥ 10 лет +- `GET /api/analytics/doctors/{doctorId}/patients` - пациенты врача (сортировка по ФИО) +- `GET /api/analytics/appointments/stats/monthly` - статистика повторных приемов за месяц +- `GET /api/analytics/patients/multiple-doctors` - пациенты старше 30 лет у нескольких врачей +- `GET /api/analytics/appointments/by-room` - приемы в кабинете за текущий месяц + +**Слой приложения:** +- DTO для передачи данных (CreateUpdateDto, Dto) +- Сервисы с бизнес-логикой +- AutoMapper для маппинга сущностей + +## Лабораторная работа №3 + +### Цель +Подключение базы данных и оркестрация запуска. + +### Реализация +**Entity Framework Core:** +- `PolyclinicDbContext` - контекст базы данных +- Репозитории для каждой сущности +- Миграция `InitialCreate` с созданием таблиц и начальными данными -### Структура проекта +**Aspire оркестратор (`Polyclinic.AppHost`):** +- SQL Server контейнер (`polyclinic-sql-server`) +- База данных `PolyclinicDb` +- API Host с зависимостью от БД +## Структура проекта + +``` Polyclinic (Solution) │ -├── Polyclinic.Domain (Class Library) +├── Polyclinic.Domain (Class Library) - Доменные сущности │ ├── Entities/ -│ │ ├── Appointment.cs -│ │ ├── Doctor.cs -│ │ ├── Patient.cs -│ │ └── Specialization.cs -│ │ -│ └── Enums/ -│ ├── BloodGroup.cs -│ ├── Gender.cs -│ └── RhFactor.cs +│ │ ├── Appointment.cs +│ │ ├── Doctor.cs +│ │ ├── Patient.cs +│ │ └── Specialization.cs +│ ├── Enums/ +│ │ ├── BloodGroup.cs +│ │ ├── Gender.cs +│ │ └── RhFactor.cs +│ └── IRepository.cs │ -└── Polyclinic.Tests (xUnit) - ├── PolyclinicFixture.cs - ├── PolyclinicTests.cs - └── TestConstants.cs - - - +├── Polyclinic.Application.Contracts (Class Library) - Контракты и DTO +│ ├── IApplicationService.cs +│ ├── IAnalyticsService.cs +│ ├── Appointments/ +│ │ ├── AppointmentDto.cs +│ │ └── AppointmentCreateUpdateDto.cs +│ ├── Doctors/ +│ │ ├── DoctorDto.cs +│ │ └── DoctorCreateUpdateDto.cs +│ ├── Patients/ +│ │ ├── PatientDto.cs +│ │ └── PatientCreateUpdateDto.cs +│ ├── Specializations/ +│ │ ├── SpecializationDto.cs +│ │ └── SpecializationCreateUpdateDto.cs +│ └── Analytics/ +│ └── MonthlyAppointmentStatsDto.cs +│ +├── Polyclinic.Application (Class Library) - Реализация сервисов +│ ├── PolyclinicProfile.cs (AutoMapper) +│ └── Services/ +│ ├── AnalyticsService.cs +│ ├── AppointmentService.cs +│ ├── DoctorService.cs +│ ├── PatientService.cs +│ └── SpecializationService.cs +│ +├── Polyclinic.Infrastructure.EfCore (Class Library) - Инфраструктура БД +│ ├── PolyclinicDbContext.cs +│ ├── Migrations/ +│ │ └── InitialCreate.cs +│ └── Repositories/ +│ ├── AppointmentRepository.cs +│ ├── DoctorRepository.cs +│ ├── PatientRepository.cs +│ └── SpecializationRepository.cs +│ +├── Polyclinic.Api.Host (ASP.NET Core Web API) - HTTP API +│ ├── Program.cs +│ └── Controllers/ +│ ├── CrudControllerBase.cs +│ ├── AnalyticsController.cs +│ ├── AppointmentController.cs +│ ├── DoctorController.cs +│ ├── PatientController.cs +│ └── SpecializationController.cs +│ +├── Polyclinic.AppHost (Aspire Host) - Оркестратор +│ └── AppHost.cs +│ +├── Polyclinic.ServiceDefaults (Class Library) - Общие настройки Aspire +│ └── Extensions.cs +│ +└── Polyclinic.Tests (xUnit) - Модульные тесты + ├── PolyclinicFixture.cs + ├── PolyclinicTests.cs + └── TestConstants.cs +``` \ No newline at end of file From c55ab388fa2523d9aac13165a34b224b2d8fd6c4 Mon Sep 17 00:00:00 2001 From: Tattaboe Date: Tue, 17 Feb 2026 14:06:29 +0400 Subject: [PATCH 7/7] Add: validation attributes in CreateUpdate Dtos --- Polyclinic/Polyclinic.Api.Host/Program.cs | 2 -- .../AppointmentCreateUpdateDto.cs | 15 ++++++++++++- .../Doctors/DoctorCreateUpdateDto.cs | 17 +++++++++++++- .../Patients/PatientCreateUpdateDto.cs | 22 +++++++++++++++++++ .../SpecializationCreateUpdateDto.cs | 11 +++++++++- 5 files changed, 62 insertions(+), 5 deletions(-) diff --git a/Polyclinic/Polyclinic.Api.Host/Program.cs b/Polyclinic/Polyclinic.Api.Host/Program.cs index 1b7bdf109..1ecc6f15d 100644 --- a/Polyclinic/Polyclinic.Api.Host/Program.cs +++ b/Polyclinic/Polyclinic.Api.Host/Program.cs @@ -42,8 +42,6 @@ }); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(c => { - c.UseInlineDefinitionsForEnums(); - var assemblies = AppDomain.CurrentDomain.GetAssemblies() .Where(a => a.GetName().Name!.StartsWith("Polyclinic")) .Distinct(); diff --git a/Polyclinic/Polyclinic.Application.Contracts/Appointments/AppointmentCreateUpdateDto.cs b/Polyclinic/Polyclinic.Application.Contracts/Appointments/AppointmentCreateUpdateDto.cs index 839797695..cca75b46a 100644 --- a/Polyclinic/Polyclinic.Application.Contracts/Appointments/AppointmentCreateUpdateDto.cs +++ b/Polyclinic/Polyclinic.Application.Contracts/Appointments/AppointmentCreateUpdateDto.cs @@ -1,4 +1,6 @@ -namespace Polyclinic.Application.Contracts.Appointments; +using System.ComponentModel.DataAnnotations; + +namespace Polyclinic.Application.Contracts.Appointments; /// /// DTO для создания и обновления записи на прием @@ -9,9 +11,20 @@ /// Идентификатор пациента /// Идентификатор врача public record AppointmentCreateUpdateDto( + [Required(ErrorMessage = "Дата и время приема обязательны")] DateTime AppointmentDateTime, + + [Required(ErrorMessage = "Номер кабинета обязателен")] + [MaxLength(10, ErrorMessage = "Номер кабинета не должен превышать 10 символов")] string RoomNumber, + bool IsRepeat, + + [Required(ErrorMessage = "Необходимо указать пациента")] + [Range(1, int.MaxValue, ErrorMessage = "Некорректный идентификатор пациента")] int PatientId, + + [Required(ErrorMessage = "Необходимо указать врача")] + [Range(1, int.MaxValue, ErrorMessage = "Некорректный идентификатор врача")] int DoctorId ); \ No newline at end of file diff --git a/Polyclinic/Polyclinic.Application.Contracts/Doctors/DoctorCreateUpdateDto.cs b/Polyclinic/Polyclinic.Application.Contracts/Doctors/DoctorCreateUpdateDto.cs index beb1f03e0..d885fcada 100644 --- a/Polyclinic/Polyclinic.Application.Contracts/Doctors/DoctorCreateUpdateDto.cs +++ b/Polyclinic/Polyclinic.Application.Contracts/Doctors/DoctorCreateUpdateDto.cs @@ -1,4 +1,6 @@ -namespace Polyclinic.Application.Contracts.Doctors; +using System.ComponentModel.DataAnnotations; + +namespace Polyclinic.Application.Contracts.Doctors; /// /// DTO для создания и обновления врача @@ -9,9 +11,22 @@ /// Идентификатор специализации /// Стаж работы (лет) public record DoctorCreateUpdateDto( + [Required(ErrorMessage = "Номер паспорта обязателен")] + [MaxLength(20, ErrorMessage = "Номер паспорта не должен превышать 20 символов")] string PassportNumber, + + [Required(ErrorMessage = "ФИО обязательно")] + [MaxLength(150, ErrorMessage = "ФИО не должно превышать 150 символов")] string FullName, + + [Required(ErrorMessage = "Дата рождения обязательна")] + [DataType(DataType.Date)] DateTime BirthDate, + + [Required(ErrorMessage = "Необходимо указать специализацию")] + [Range(1, int.MaxValue, ErrorMessage = "Некорректный идентификатор специализации")] int SpecializationId, + + [Range(0, 100, ErrorMessage = "Стаж работы должен быть от 0 до 100 лет")] int ExperienceYears ); \ No newline at end of file diff --git a/Polyclinic/Polyclinic.Application.Contracts/Patients/PatientCreateUpdateDto.cs b/Polyclinic/Polyclinic.Application.Contracts/Patients/PatientCreateUpdateDto.cs index 51f9e71dd..9e9f6b5be 100644 --- a/Polyclinic/Polyclinic.Application.Contracts/Patients/PatientCreateUpdateDto.cs +++ b/Polyclinic/Polyclinic.Application.Contracts/Patients/PatientCreateUpdateDto.cs @@ -1,4 +1,5 @@ using Polyclinic.Domain.Enums; +using System.ComponentModel.DataAnnotations; namespace Polyclinic.Application.Contracts.Patients; @@ -14,12 +15,33 @@ namespace Polyclinic.Application.Contracts.Patients; /// Резус-фактор /// Контактный телефон public record PatientCreateUpdateDto( + [Required(ErrorMessage = "Номер паспорта обязателен")] + [MaxLength(20, ErrorMessage = "Номер паспорта не должен превышать 20 символов")] string PassportNumber, + + [Required(ErrorMessage = "ФИО обязательно")] + [MaxLength(150, ErrorMessage = "ФИО не должно превышать 150 символов")] string FullName, + + [Required(ErrorMessage = "Пол обязателен")] Gender Gender, + + [Required(ErrorMessage = "Дата рождения обязательна")] + [DataType(DataType.Date)] DateTime BirthDate, + + [Required(ErrorMessage = "Адрес обязателен")] + [MaxLength(250, ErrorMessage = "Адрес не должен превышать 250 символов")] string Address, + + [Required(ErrorMessage = "Группа крови обязательна")] BloodGroup BloodGroup, + + [Required(ErrorMessage = "Резус-фактор обязателен")] RhFactor RhFactor, + + [Required(ErrorMessage = "Телефон обязателен")] + [Phone(ErrorMessage = "Некорректный формат телефона")] + [MaxLength(20, ErrorMessage = "Телефон не должен превышать 20 символов")] string PhoneNumber ); \ No newline at end of file diff --git a/Polyclinic/Polyclinic.Application.Contracts/Specializations/SpecializationCreateUpdateDto.cs b/Polyclinic/Polyclinic.Application.Contracts/Specializations/SpecializationCreateUpdateDto.cs index 3df679b9d..251da3201 100644 --- a/Polyclinic/Polyclinic.Application.Contracts/Specializations/SpecializationCreateUpdateDto.cs +++ b/Polyclinic/Polyclinic.Application.Contracts/Specializations/SpecializationCreateUpdateDto.cs @@ -1,4 +1,6 @@ -namespace Polyclinic.Application.Contracts.Specializations; +using System.ComponentModel.DataAnnotations; + +namespace Polyclinic.Application.Contracts.Specializations; /// /// DTO для создания и обновления специализации @@ -7,7 +9,14 @@ /// Описание специализации /// Код специализации public record SpecializationCreateUpdateDto( + [Required(ErrorMessage = "Название специализации обязательно")] + [MaxLength(100, ErrorMessage = "Название не должно превышать 100 символов")] string Name, + + [MaxLength(500, ErrorMessage = "Описание не должно превышать 500 символов")] string Description, + + [Required(ErrorMessage = "Код специализации обязателен")] + [MaxLength(20, ErrorMessage = "Код не должен превышать 20 символов")] string Code ); \ No newline at end of file