docs: добавил подробную документацию по backend
🚀 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
🚀 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
This commit is contained in:
+581
@@ -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<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.
|
||||
Reference in New Issue
Block a user