Files
UniVerse/docs/backend.md
T
serega404 98ad8ae74f
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Successful in 10s
🚀 Create and publish a Docker image / Build & publish backend image (push) Successful in 14s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Successful in 17s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Successful in 8s
docs: добавил подробную документацию по backend
2026-05-13 00:48:50 +03:00

582 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.sln).
## Быстрый старт для чтения кода
Начинай с этих файлов:
1. [`backend/UniVerse.Api/Program.cs`](../backend/UniVerse.Api/Program.cs) - composition root: DI, middleware, auth, Swagger, фоновые сервисы.
2. [`backend/UniVerse.Api/Controllers`](../backend/UniVerse.Api/Controllers) - HTTP API и роли доступа.
3. [`backend/UniVerse.Application/Interfaces`](../backend/UniVerse.Application/Interfaces) - контракты бизнес-сервисов.
4. [`backend/UniVerse.Infrastructure/Services`](../backend/UniVerse.Infrastructure/Services) - реализация сценариев.
5. [`backend/UniVerse.Domain/Entities`](../backend/UniVerse.Domain/Entities) - доменная модель.
6. [`backend/UniVerse.Infrastructure/Data/AppDbContext.cs`](../backend/UniVerse.Infrastructure/Data/AppDbContext.cs) и [`Configurations`](../backend/UniVerse.Infrastructure/Data/Configurations) - схема PostgreSQL.
## Проекты и ответственность
```mermaid
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-запроса
```mermaid
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`](../backend/UniVerse.Api/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`, например:
```bash
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
Из корня репозитория:
```bash
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`.
Тесты:
```bash
cd backend
dotnet test UniVerse.sln
```
Docker compose есть в корне:
- [`docker-compose-test.yml`](../docker-compose-test.yml) - сборка локальных образов;
- [`docker-compose-prod.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.
Вход:
```mermaid
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`:
```http
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 | статус последней синхронизации |
## Доменная модель
```mermaid
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`](../backend/UniVerse.Infrastructure/Data/AppDbContext.cs). PostgreSQL enums регистрируются для ролей, типов тегов, LLM-статусов, сентимента и типов транзакций.
## Основные сценарии
### Запись на лекцию
```mermaid
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-анализ
```mermaid
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`](../backend/UniVerse.Infrastructure/Data/AchievementCatalogSeeder.cs).
### Уведомления
```mermaid
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
```mermaid
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` |
Формат ошибки:
```json
{
"type": "https://httpstatuses.com/404",
"title": "Not Found",
"status": 404,
"detail": "Lecture with id 10 was not found.",
"traceId": "..."
}
```
## Фильтрация и пагинация
Общий формат пагинации:
```json
{
"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-сценарий
Обычный путь изменения:
1. Добавить или изменить entity/enum в `UniVerse.Domain`, если меняется модель данных.
2. Добавить DTO в `UniVerse.Application/DTOs`.
3. Добавить контракт в `UniVerse.Application/Interfaces`.
4. Реализовать сервис в `UniVerse.Infrastructure/Services`.
5. Зарегистрировать сервис в DI в `Program.cs`.
6. Добавить endpoint в controller.
7. Если меняется БД, создать EF migration через `dotnet ef`, не редактировать миграции вручную.
8. Добавить тесты авторизации/бизнес-логики.
9. Проверить Swagger JSON и, если нужно, frontend-контракт.
Команда миграции:
```bash
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` в нескольких `POST` endpoint-ах передает `{ id = 0 }`, хотя созданная сущность уже известна. Если клиенту важен `Location` header, это стоит поправить.
- `SyncStatusDto` хранится в памяти процесса, не в БД.
- `ReviewLlmStatus.Rejected` есть в enum, но текущий `LlmAnalysisService` не переводит отзывы в rejected.
- В `CourseTag`, `LectureEnrollment`, `UserAchievement` entity есть поле `Id`, но `AppDbContext.OnModelCreating` дополнительно задает составные ключи для этих сущностей после применения конфигураций. При изменении модели обязательно сверяй snapshot/миграцию.
- `Secure = true` у refresh cookie означает, что для cookie-flow в браузере нужен HTTPS. В локальной разработке через plain HTTP это может влиять на поведение cookie.
## Ментальная модель
Если совсем коротко:
```mermaid
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.