28 KiB
Backend UniVerse
Этот документ помогает быстро вкатиться в backend UniVerse: понять слои, основные сущности, API, фоновые процессы, интеграции и точки расширения. Диаграммы написаны в Mermaid, поэтому их можно смотреть прямо в IDE/Markdown-просмотрщике с поддержкой Mermaid.
Что делает backend
Backend UniVerse обслуживает платформу открытых лекций:
- хранит пользователей, роли, профили студентов и преподавателей;
- ведет каталог курсов, тегов, локаций и лекций;
- позволяет студентам записываться на лекции и оставлять отзывы;
- анализирует отзывы через LLM в фоне;
- начисляет монеты, XP и достижения;
- отправляет и планирует уведомления;
- синхронизирует расписание и аудитории из Modeus;
- публикует REST API и Swagger/OpenAPI.
Основной solution: backend/UniVerse.sln.
Быстрый старт для чтения кода
Начинай с этих файлов:
backend/UniVerse.Api/Program.cs- composition root: DI, middleware, auth, Swagger, фоновые сервисы.backend/UniVerse.Api/Controllers- HTTP API и роли доступа.backend/UniVerse.Application/Interfaces- контракты бизнес-сервисов.backend/UniVerse.Infrastructure/Services- реализация сценариев.backend/UniVerse.Domain/Entities- доменная модель.backend/UniVerse.Infrastructure/Data/AppDbContext.csиConfigurations- схема PostgreSQL.
Проекты и ответственность
flowchart LR
Client[Frontend / API client]
Api[UniVerse.Api<br/>Controllers, middleware, hosted services]
App[UniVerse.Application<br/>DTO, interfaces, mappings]
Infra[UniVerse.Infrastructure<br/>EF Core, services, external clients]
Domain[UniVerse.Domain<br/>Entities, enums, exceptions]
Db[(PostgreSQL)]
Llm[OpenAI-compatible LLM]
Modeus[Modeus / schedule.rdcenter.ru]
Email[SMTP]
Client --> Api
Api --> App
Api --> Infra
Infra --> App
Infra --> Domain
App --> Domain
Infra --> Db
Infra --> Llm
Infra --> Modeus
Infra --> Email
| Проект | Что внутри | Зависит от |
|---|---|---|
UniVerse.Api |
ASP.NET Core Web API: контроллеры, JWT, CORS, Swagger, middleware, background services | Application, Infrastructure, ServiceDefaults |
UniVerse.Application |
DTO, интерфейсы сервисов, mapping extensions | Domain |
UniVerse.Domain |
Entities, enums, доменные исключения | ничего |
UniVerse.Infrastructure |
EF Core, PostgreSQL, сервисы, LLM/Modeus/SMTP-клиенты, Quartz scheduler | Domain, Application |
UniVerse.AppHost |
.NET Aspire host для совместного запуска API и frontend dev server | API и frontend как внешняя команда |
UniVerse.Api.Tests |
xUnit-тесты авторизации, Swagger и геймификации | API |
Жизненный цикл HTTP-запроса
sequenceDiagram
participant C as Client
participant M as ASP.NET middleware
participant Auth as JWT auth
participant Ctrl as Controller
participant Svc as Application interface / Infrastructure service
participant Db as AppDbContext
C->>M: HTTP request /api/v1/...
M->>M: RequestLoggingMiddleware
M->>M: ExceptionHandlingMiddleware
M->>Auth: UseAuthentication / UseAuthorization
Auth-->>Ctrl: ClaimsPrincipal with roles
Ctrl->>Svc: DTO/request + current user id
Svc->>Db: EF Core query/command
Db-->>Svc: entities
Svc-->>Ctrl: DTO/result
Ctrl-->>C: JSON response
Пайплайн в Program.cs:
RequestLoggingMiddlewareпишет входящие запросы.ExceptionHandlingMiddlewareпереводит доменные исключения вapplication/problem+json.- CORS берет origins из
Cors:Origins. - JWT Bearer проверяет issuer, audience, lifetime и signing key.
- Swagger доступен в
Developmentпо/api/docs, JSON -/api/docs/v1/swagger.json.
Конфигурация
Основные секции:
| Секция | Назначение |
|---|---|
ConnectionStrings:DefaultConnection |
подключение к PostgreSQL |
Jwt:* |
issuer, audience, secret, TTL access/refresh токенов |
AzureAd:* |
Microsoft Entra ID OAuth flow |
Cors:Origins |
разрешенные origins frontend |
Llm:* |
OpenAI-compatible endpoint, API key, model |
ModeusApi:* |
endpoint и ключ внешнего расписания |
Email:Smtp:* |
SMTP-провайдер уведомлений |
Gamification:XpThresholds |
пороги уровней по XP |
Aspire:Enabled |
включает service defaults при запуске через AppHost |
Для окружений удобнее задавать переменные в формате Section__Key, например:
ConnectionStrings__DefaultConnection="Host=localhost;Port=5432;Database=universe;Username=postgres;Password=postgres"
Jwt__Secret="local-secret-at-least-32-characters"
Llm__ApiKey="..."
ModeusApi__ApiKey="..."
Email__Smtp__Host="smtp.example.com"
Важно: секреты не должны попадать в документацию, README, коммиты и логи. В dev-файлах могут быть локальные значения, но для реального запуска используй переменные окружения или secret storage.
Как запустить backend
Из корня репозитория:
cd backend
dotnet restore UniVerse.sln
dotnet ef database update --project UniVerse.Infrastructure --startup-project UniVerse.Api
dotnet run --project UniVerse.Api --launch-profile http
По launch profile API слушает http://localhost:5019, Swagger UI: http://localhost:5019/api/docs.
Тесты:
cd backend
dotnet test UniVerse.sln
Docker compose есть в корне:
docker-compose-test.yml- сборка локальных образов;docker-compose-prod.yml- запуск published images.
Авторизация и роли
Роли заданы enum UserRole: Student, Teacher, Admin.
Токены:
- access token - JWT Bearer, передается в
Authorization: Bearer <token>; - refresh token - HttpOnly cookie
refreshToken; - logout отзывает refresh token в БД, но уже выданный access token живет до истечения TTL.
Вход:
sequenceDiagram
participant U as User browser
participant F as Frontend
participant A as UniVerse.Api
participant MS as Microsoft Entra ID
participant DB as PostgreSQL
U->>F: Нажимает "Войти"
F->>A: GET /api/v1/auth/login/microsoft?returnUrl=...
A->>A: Генерирует state и cookie
A-->>U: 302 redirect на Microsoft
U->>MS: OAuth authorize
MS-->>A: GET /api/v1/auth/callback/microsoft?code&state
A->>A: Проверяет state
A->>MS: Обменивает code на token
A->>DB: Upsert user, roles, refresh token
A-->>U: refreshToken cookie + redirect с access token во fragment
Dev-вход доступен только в Development:
POST /api/v1/auth/login/dev
Content-Type: application/json
{
"email": "student@example.com",
"displayName": "Student",
"roles": ["Student"]
}
Карта API
Базовый префикс: /api/v1.
| Область | Endpoint | Доступ | Назначение |
|---|---|---|---|
| Auth | POST /auth/login/microsoft |
public | обмен authorization code на токены |
| Auth | GET /auth/login/microsoft |
public | server-driven redirect на Microsoft |
| Auth | GET /auth/callback/microsoft |
public | OAuth callback |
| Auth | POST /auth/login/dev |
public, только Development | dev login |
| Auth | POST /auth/refresh |
public + refresh cookie | обновить access token |
| Auth | POST /auth/logout |
auth | отозвать refresh token |
| Auth | GET /auth/me |
auth | текущий пользователь |
| Users | GET /users |
Admin | список пользователей |
| Users | GET /users/{id} |
auth | профиль пользователя |
| Users | PUT /users/{id} |
владелец или Admin | обновить displayName, avatarUrl |
| Users | GET /users/{id}/stats |
auth | XP, уровень, монеты, посещения |
| Users | GET /users/{id}/reviews |
auth | отзывы пользователя |
| Users | GET /users/{id}/achievements |
auth | достижения пользователя |
| Users | GET /users/{id}/transactions |
владелец или Admin | история монет |
| Users | PATCH /users/{id}/role |
Admin | заменить набор ролей |
| Users | PATCH /users/{id}/active |
Admin | активировать/деактивировать пользователя |
| Courses | GET /courses |
auth | список курсов с фильтрами |
| Courses | GET /courses/{id} |
auth | курс с тегами |
| Courses | POST /courses |
Admin | создать курс |
| Courses | PUT /courses/{id} |
Admin | обновить курс |
| Courses | DELETE /courses/{id} |
Admin | удалить курс |
| Courses | POST /courses/{id}/tags |
Admin | привязать тег |
| Courses | DELETE /courses/{id}/tags/{tagId} |
Admin | отвязать тег |
| Lectures | GET /lectures |
auth | каталог лекций |
| Lectures | GET /lectures/{id} |
auth | детальная карточка |
| Lectures | POST /lectures |
Admin | создать лекцию |
| Lectures | PUT /lectures/{id} |
Admin, Teacher | обновить лекцию |
| Lectures | DELETE /lectures/{id} |
Admin | удалить лекцию |
| Lectures | POST /lectures/{id}/enroll |
Student | записаться |
| Lectures | DELETE /lectures/{id}/enroll |
Student | отменить запись |
| Lectures | PATCH /lectures/{id}/attendance/{userId} |
Admin, Teacher | отметить посещение |
| Lectures | GET /lectures/{id}/enrollments |
Admin, Teacher | записавшиеся |
| Lectures | GET /lectures/{id}/reviews |
auth | отзывы по лекции |
| Reviews | POST /reviews |
Student | создать отзыв |
| Reviews | GET /reviews/{id} |
auth | получить отзыв |
| Reviews | PUT /reviews/{id} |
владелец | обновить отзыв и сбросить LLM-статус |
| Reviews | DELETE /reviews/{id} |
владелец или Admin | удалить отзыв |
| Reviews | GET /reviews/pending |
Admin | очередь LLM-анализа |
| Reviews | POST /reviews/{id}/reanalyze |
Admin | поставить отзыв на повторный анализ |
| Tags | GET /tags |
auth | список тегов |
| Tags | GET /tags/tree |
auth | дерево тегов |
| Tags | GET /tags/{id} |
auth | тег по id |
| Tags | POST /tags |
Admin | создать тег |
| Tags | PUT /tags/{id} |
Admin | обновить тег |
| Tags | DELETE /tags/{id} |
Admin | удалить тег |
| Locations | GET /locations |
auth | список локаций |
| Locations | GET /locations/{id} |
auth | локация по id |
| Locations | POST /locations |
Admin | создать локацию |
| Locations | PUT /locations/{id} |
Admin | обновить локацию |
| Locations | DELETE /locations/{id} |
Admin | удалить локацию |
| Achievements | GET /achievements |
auth | каталог достижений |
| Achievements | GET /achievements/{id} |
auth | достижение по id |
| Achievements | POST /achievements |
Admin | создать достижение |
| Achievements | PUT /achievements/{id} |
Admin | обновить достижение |
| Achievements | DELETE /achievements/{id} |
Admin | удалить достижение |
| Notifications | GET /notifications |
auth | уведомления текущего пользователя |
| Notifications | PATCH /notifications/read-all |
auth | отметить свои уведомления прочитанными |
| Notifications | POST /notifications/send |
Admin | отправить уведомление сразу |
| Notifications | POST /notifications/schedule |
Admin | запланировать уведомление через Quartz |
| Sync | POST /sync/schedule |
Admin | синхронизация лекций из Modeus |
| Sync | POST /sync/rooms |
Admin | синхронизация аудиторий |
| Sync | POST /sync/employees?fullname=... |
Admin | поиск сотрудников |
| Sync | GET /sync/status |
Admin | статус последней синхронизации |
Доменная модель
erDiagram
USERS ||--o{ USER_ROLES : has
USERS ||--o| STUDENT_PROFILES : may_have
USERS ||--o| TEACHER_PROFILES : may_have
USERS ||--o{ REFRESH_TOKENS : owns
USERS ||--o{ LECTURE_ENROLLMENTS : enrolls
USERS ||--o{ REVIEWS : writes
USERS ||--o{ USER_ACHIEVEMENTS : earns
USERS ||--o{ COIN_TRANSACTIONS : receives
USERS ||--o{ USER_NOTIFICATIONS : receives
COURSES ||--o{ LECTURES : contains
COURSES ||--o{ COURSE_TAGS : tagged
TAGS ||--o{ COURSE_TAGS : assigned
TAGS ||--o{ TAGS : parent_of
LOCATIONS ||--o{ LECTURES : hosts
LECTURES ||--o{ LECTURE_ENROLLMENTS : has
LECTURES ||--o{ REVIEWS : receives
USERS ||--o{ LECTURES : teaches
ACHIEVEMENTS ||--o{ USER_ACHIEVEMENTS : awarded_as
REVIEWS ||--o{ COIN_TRANSACTIONS : may_reward
ACHIEVEMENTS ||--o{ COIN_TRANSACTIONS : may_reward
Ключевые таблицы и поля:
| Сущность | Смысл |
|---|---|
users |
аккаунт: email, display name, avatar, active flag, Microsoft id, XP, coins |
user_roles |
join table ролей; у пользователя может быть несколько ролей |
student_profiles, teacher_profiles |
дополнительные данные профиля |
refresh_tokens |
refresh token, expiry, revoked flag |
courses |
дисциплины/курсы; могут быть синхронизированы из Modeus |
tags |
иерархические теги с типом: institute/faculty/subject/topic/etc |
course_tags |
связь many-to-many между курсами и тегами |
lectures |
открытые лекции: курс, преподаватель, локация, время, формат, вместимость |
lecture_enrollments |
записи студентов, флаг посещения |
reviews |
отзывы студентов и результат LLM-анализа |
achievements |
каталог достижений и условие получения |
user_achievements |
полученные пользователем достижения |
coin_transactions |
история начисления монет/XP |
user_notifications |
in-app уведомления пользователя |
EF Core приводит имена таблиц и колонок к snake_case в AppDbContext. PostgreSQL enums регистрируются для ролей, типов тегов, LLM-статусов, сентимента и типов транзакций.
Основные сценарии
Запись на лекцию
sequenceDiagram
participant S as Student
participant C as LecturesController
participant LS as LectureService
participant GS as GamificationService
participant DB as PostgreSQL
S->>C: POST /api/v1/lectures/{id}/enroll
C->>LS: EnrollAsync(lectureId, currentUserId)
LS->>DB: Load lecture with enrollments
LS->>LS: Проверить isOpen, capacity, duplicate
LS->>DB: Insert lecture_enrollment
LS->>GS: CheckAndAwardAchievementsAsync(userId)
GS->>DB: Проверить условия достижений
GS-->>C: done
C-->>S: 204 No Content
Условия:
- лекция должна быть открыта (
IsOpen = true); - если
MaxEnrollments > 0, число записей не должно превышать лимит; - повторная запись дает
409 Conflict; - после записи проверяются достижения.
Посещение лекции
PATCH /api/v1/lectures/{id}/attendance/{userId} доступен Admin и Teacher. Сервис меняет LectureEnrollment.Attended. Если attended = true, запускается проверка достижений. В текущей реализации отдельное начисление монет за посещение не вызывается напрямую, но достижения могут начислить награду.
Отзыв и LLM-анализ
sequenceDiagram
participant S as Student
participant RC as ReviewsController
participant RS as ReviewService
participant BG as LlmProcessingBackgroundService
participant LLM as LlmAnalysisService
participant Client as LlmClient
participant G as GamificationService
participant DB as PostgreSQL
S->>RC: POST /api/v1/reviews
RC->>RS: CreateAsync(userId, request)
RS->>DB: Insert review with LlmStatus=Pending
RS->>G: CheckAndAwardAchievementsAsync(userId)
RC-->>S: 201 ReviewDto
loop каждые 2 минуты
BG->>LLM: ProcessPendingReviewsAsync()
LLM->>DB: взять до 10 Pending отзывов
LLM->>Client: AnalyzeReviewAsync(text, lecture context)
Client-->>LLM: qualityScore, sentiment, tags, isInformative
LLM->>DB: сохранить результат, LlmStatus=Analyzed
alt отзыв информативный
LLM->>G: AwardCoinsAsync(..., 10, ReviewReward)
end
LLM->>G: CheckAndAwardAchievementsAsync(userId)
end
LLM-запись хранит:
LlmStatus:Pending,Analyzed,Rejected;Sentiment:Positive,Neutral,Negative;QualityScore;IsInformative;LlmTags.
Если LLM-клиент падает, статус остается Pending, а фоновый сервис попробует снова на следующем цикле.
Геймификация
GamificationService делает две вещи:
- начисляет монеты и XP через
AwardCoinsAsync; - проверяет каталог достижений через
CheckAndAwardAchievementsAsync.
Условия достижений хранятся строкой type:value, например:
| Условие | Что проверяется |
|---|---|
first_activity:1 |
есть запись, отзыв или посещение |
lectures_attended:N |
посещено минимум N лекций |
reviews_written:N |
написано минимум N отзывов |
lectures_registered:N |
оформлено минимум N записей |
active_registrations:N |
есть N будущих записей |
attendance_streak_weeks:N |
посещения N недель подряд |
coins_earned:N |
получено минимум N положительных монет |
level_reached:N |
достигнут уровень N |
profile_completed:1 |
заполнены DisplayName и AvatarUrl |
Каталог базовых достижений заполняется при старте через AchievementCatalogHostedService и AchievementCatalogSeeder.
Уведомления
flowchart LR
Admin[Admin API request]
Achievement[Achievement earned]
NotificationService[NotificationService]
Db[(user_notifications)]
Provider[INotificationProvider]
Email[EmailNotificationProvider / SMTP]
Quartz[QuartzNotificationScheduler]
Job[NotificationJob]
Admin -->|POST /notifications/send| NotificationService
Admin -->|POST /notifications/schedule| Quartz
Achievement --> NotificationService
NotificationService --> Db
NotificationService --> Provider
Provider --> Email
Quartz --> Job
Job --> NotificationService
Сейчас основной канал - email. In-app уведомления хранятся в user_notifications; пользователь может получить их через GET /api/v1/notifications и отметить все прочитанными через PATCH /api/v1/notifications/read-all.
Синхронизация Modeus
flowchart TD
Admin[Admin] --> Sync[SyncController]
Sync --> Service[ScheduleSyncService]
Service --> Modeus[ModeusApiClient]
Modeus --> External[schedule.rdcenter.ru]
Service --> Courses[(courses)]
Service --> Lectures[(lectures)]
Service --> Locations[(locations)]
Service --> Status[static _lastStatus]
POST /api/v1/sync/schedule:
- запрашивает события из Modeus;
- upsert-ит курс по
ExternalId; - upsert-ит локацию из комнаты события;
- upsert-ит лекцию по
ExternalId; - сохраняет счетчики
created,updated,skipped.
POST /api/v1/sync/rooms импортирует аудитории в locations. POST /api/v1/sync/employees только ищет сотрудников и не создает пользователей автоматически.
Статус последней синхронизации хранится в памяти процесса (static _lastStatus), поэтому после рестарта API он снова idle.
Ошибки и ответы
Доменные исключения:
| Исключение | HTTP |
|---|---|
NotFoundException |
404 Not Found |
ForbiddenException |
403 Forbidden |
ConflictException |
409 Conflict |
UnauthorizedAccessException |
401 Unauthorized |
| неизвестная ошибка | 500 Internal Server Error |
Формат ошибки:
{
"type": "https://httpstatuses.com/404",
"title": "Not Found",
"status": 404,
"detail": "Lecture with id 10 was not found.",
"traceId": "..."
}
Фильтрация и пагинация
Общий формат пагинации:
{
"items": [],
"total": 0,
"page": 1,
"pageSize": 20,
"totalPages": 0
}
Частые query-параметры:
page,pageSize- для пагинации;search- текстовый поиск;tagId,courseId,teacherId- фильтры каталога;dateFrom,dateTo- даты лекций;format-OnlineилиOffline;isOpen- открыта ли запись.
Swagger и тесты
Swagger:
- UI:
/api/docs; - JSON:
/api/docs/v1/swagger.json; AuthorizeOperationFilterдобавляет Bearer security только к защищенным endpoint-ам и дописывает требуемые роли в описание.
Тесты:
EndpointAuthorizationTestsпроверяет, что защищенные endpoint-ы возвращают401анонимам и403неправильным ролям;SwaggerDocumentTestsпроверяет генерацию OpenAPI и security metadata;GamificationServiceTestsпроверяет правила достижений/наград.
Как добавить новый backend-сценарий
Обычный путь изменения:
- Добавить или изменить entity/enum в
UniVerse.Domain, если меняется модель данных. - Добавить DTO в
UniVerse.Application/DTOs. - Добавить контракт в
UniVerse.Application/Interfaces. - Реализовать сервис в
UniVerse.Infrastructure/Services. - Зарегистрировать сервис в DI в
Program.cs. - Добавить endpoint в controller.
- Если меняется БД, создать EF migration через
dotnet ef, не редактировать миграции вручную. - Добавить тесты авторизации/бизнес-логики.
- Проверить Swagger JSON и, если нужно, frontend-контракт.
Команда миграции:
cd backend
dotnet ef migrations add NameOfMigration --project UniVerse.Infrastructure --startup-project UniVerse.Api
dotnet ef database update --project UniVerse.Infrastructure --startup-project UniVerse.Api
Места, на которые стоит обратить внимание
GET /api/v1/users/{id}/enrollmentsсейчас проверяет доступ, но возвращает пустой200 OK; полноценная выдача записей пользователя еще не реализована.CreatedAtActionв несколькихPOSTendpoint-ах передает{ id = 0 }, хотя созданная сущность уже известна. Если клиенту важенLocationheader, это стоит поправить.SyncStatusDtoхранится в памяти процесса, не в БД.ReviewLlmStatus.Rejectedесть в enum, но текущийLlmAnalysisServiceне переводит отзывы в rejected.- В
CourseTag,LectureEnrollment,UserAchievemententity есть полеId, ноAppDbContext.OnModelCreatingдополнительно задает составные ключи для этих сущностей после применения конфигураций. При изменении модели обязательно сверяй snapshot/миграцию. Secure = trueу refresh cookie означает, что для cookie-flow в браузере нужен HTTPS. В локальной разработке через plain HTTP это может влиять на поведение cookie.
Ментальная модель
Если совсем коротко:
flowchart LR
Auth[Auth: Microsoft/dev login] --> User[User + roles]
User --> Catalog[Catalog: courses, tags, locations]
Catalog --> Lecture[Lectures]
User --> Enrollment[Enrollments]
Lecture --> Enrollment
Enrollment --> Attendance[Attendance]
Lecture --> Review[Reviews]
Review --> Llm[LLM analysis]
Llm --> Rewards[Coins + XP]
Attendance --> Achievements[Achievements]
Enrollment --> Achievements
Review --> Achievements
Rewards --> Achievements
Achievements --> Notifications[Notifications]
Modeus[Modeus sync] --> Catalog
Modeus --> Lecture
Почти все пользовательские сценарии проходят через пользователя с ролью, меняют лекции/записи/отзывы, а потом запускают геймификацию. Внешние интеграции - LLM, Modeus и SMTP - изолированы в Infrastructure и подключены через интерфейсы Application.