From 98ad8ae74fe8fd1064a409aa4c93e11bc8c513ed Mon Sep 17 00:00:00 2001 From: Sergey Karmanov Date: Wed, 13 May 2026 00:48:50 +0300 Subject: [PATCH] =?UTF-8?q?docs:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=BF=D0=BE=D0=B4=D1=80=D0=BE=D0=B1=D0=BD=D1=83=D1=8E?= =?UTF-8?q?=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8E=20=D0=BF=D0=BE=20backend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/backend.md | 581 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 581 insertions(+) create mode 100644 docs/backend.md diff --git a/docs/backend.md b/docs/backend.md new file mode 100644 index 0000000..424a9cf --- /dev/null +++ b/docs/backend.md @@ -0,0 +1,581 @@ +# 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
Controllers, middleware, hosted services] + App[UniVerse.Application
DTO, interfaces, mappings] + Infra[UniVerse.Infrastructure
EF Core, services, external clients] + Domain[UniVerse.Domain
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 `; +- 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.