DEMO docs
This commit is contained in:
@@ -0,0 +1,51 @@
|
|||||||
|
# Общее описание
|
||||||
|
|
||||||
|
Платформа для грантов предназначена для упрощения и автоматизации процесса распределения грантовых
|
||||||
|
средств. Она предоставляет удобный инструмент как для организаций, желающих вложиться в общественно
|
||||||
|
полезные проекты, так и для участников, нуждающихся в финансировании. Основные задачи платформы:
|
||||||
|
|
||||||
|
- **Прозрачность:** Платформа обеспечивает открытый процесс подачи заявок, их оценки и выбора
|
||||||
|
победителей.
|
||||||
|
|
||||||
|
- **Доступность:** Участники могут легко находить актуальные конкурсы и подавать заявки, а
|
||||||
|
компании — запускать собственные грантовые программы.
|
||||||
|
|
||||||
|
- **Эффективность:** Процесс отбора и реализации проектов автоматизирован, что экономит время
|
||||||
|
участников и организаторов.
|
||||||
|
|
||||||
|
## Основные пользователи платформы:
|
||||||
|
|
||||||
|
- **Компании:** Организуют конкурсы, финансируют проекты.
|
||||||
|
|
||||||
|
- **Участники:** Представляют свои проекты, чтобы получить финансирование. При подаче заявки
|
||||||
|
участники выбирают категорию, что влияет на структуру заявки и условия участия.
|
||||||
|
|
||||||
|
- **Модераторы:** Проверяют заявки, следят за корректностью информации.
|
||||||
|
|
||||||
|
- **Эксперты:** Оценивают проекты, прошедшие модерацию, с учетом их категории.
|
||||||
|
|
||||||
|
- **Комиссия:** Принимает окончательные решения о финансировании.
|
||||||
|
|
||||||
|
- **Администраторы:** Управляют платформой и обеспечивают её стабильную работу, включая создание и
|
||||||
|
настройку категорий.
|
||||||
|
|
||||||
|
## Основные этапы работы платформы:
|
||||||
|
|
||||||
|
1. Регистрация пользователей (компании, организации, волонтеры).
|
||||||
|
|
||||||
|
2. Публикация конкурсов
|
||||||
|
|
||||||
|
3. Сбор заявок. На этапе подачи заявки пользователь выбирает категорию, что определяет дальнейшую
|
||||||
|
структуру заявки и условия участия.
|
||||||
|
|
||||||
|
4. Модерация заявок и внесение исправлений участниками.
|
||||||
|
|
||||||
|
5. Оценка проектов экспертами с учетом специфики категории.
|
||||||
|
|
||||||
|
6. Выбор победителей комиссией.
|
||||||
|
|
||||||
|
7. Реализация проектов победителями, включая публикацию отчетов.
|
||||||
|
|
||||||
|
Платформа также включает инструменты для контроля исполнения проектов, анализа их результатов и
|
||||||
|
формирования отчетности. Это позволяет компаниям видеть, как эффективно используются их средства, а
|
||||||
|
участникам — демонстрировать успешность своих инициатив.
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Основные функции платформы
|
||||||
|
|
||||||
|
## Для участников
|
||||||
|
|
||||||
|
- **Поиск конкурсов:** Участники могут просматривать доступные конкурсы.
|
||||||
|
|
||||||
|
- **Подача заявок:** Удобный интерфейс для создания и отправки заявок на участие в конкурсе.
|
||||||
|
|
||||||
|
- **Управление проектами:** Ведение проектов, отслеживание их статуса и предоставление отчетности.
|
||||||
|
|
||||||
|
- **Финансовая отчетность:** Предоставление данных о расходах по проекту через встроенные формы.
|
||||||
|
|
||||||
|
## Для организаторов
|
||||||
|
|
||||||
|
- **Создание конкурсов:** Возможность настроить параметры конкурса (цели, требования, сроки и
|
||||||
|
др.).
|
||||||
|
|
||||||
|
- **Управление заявками:** Просмотр всех поступивших заявок, их модерация и одобрение.
|
||||||
|
|
||||||
|
- **Работа с экспертами:** Назначение экспертов для оценки заявок, управление их доступами.
|
||||||
|
|
||||||
|
- **Мониторинг реализации проектов:** Контроль выполнения грантовых обязательств победителей.
|
||||||
|
|
||||||
|
## Для экспертов
|
||||||
|
|
||||||
|
- **Оценка заявок:** Просмотр и оценивание заявок участников по заданным критериям. Возможность
|
||||||
|
оставлять комментарии и замечания.
|
||||||
|
|
||||||
|
## Для модераторов
|
||||||
|
|
||||||
|
- **Проверка заявок:** Проверка заявок участников на соответствие требованиям конкурса.
|
||||||
|
|
||||||
|
- **Коммуникация с участниками:** Возможность запрашивать доработки заявок и уведомлять участников
|
||||||
|
об изменениях статуса.
|
||||||
|
|
||||||
|
## Для администраторов платформы
|
||||||
|
|
||||||
|
- **Управление пользователями:** Добавление, редактирование и удаление пользователей.
|
||||||
|
|
||||||
|
- **Мониторинг активности:** Анализ активности на платформе, выявление проблемных мест.
|
||||||
|
|
||||||
|
- **Настройка глобальных параметров:** Конфигурация технических аспектов работы системы.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Роли пользователей
|
||||||
|
|
||||||
|
## Глобальные роли
|
||||||
|
|
||||||
|
Эти роли может выдать только `root` пользователь:
|
||||||
|
|
||||||
|
- `root` Максимальный уровень доступа. С этим уровнем доступа можно обходить некоторые системы
|
||||||
|
сайта. Предполагается, что этот пользователь знает, что делает.
|
||||||
|
- `support` Имеет доступ к большому количеству данных и функций, может влиять на них, но с
|
||||||
|
ограничениями в критически важных областях.
|
||||||
|
|
||||||
|
## Сотрудники конкурса
|
||||||
|
|
||||||
|
Эти роли могут быть выданы как на отдельные конкурсы, так и глобально на все конкурсы:
|
||||||
|
|
||||||
|
- `admin` Администратор конкурса. Может настраивать конкурс и сотрудников, имеет права `moderator`
|
||||||
|
- `moderator` Просмотр и модерирование проектов, просмотр статистики конкурса, возможность
|
||||||
|
блокировки пользователей.
|
||||||
|
- `expert` Оценка проектов без доступа к настройкам или модерации.
|
||||||
|
|
||||||
|
## Сотрудники организатора
|
||||||
|
|
||||||
|
Эти роли выдаются только на конкретные организации:
|
||||||
|
|
||||||
|
- `orgAdminRole` Руководитель организации. Может настраивать организацию и её сотрудников.
|
||||||
|
- `orgMemberRole` Ответственный за заполнение проектов и отчетов.
|
||||||
|
- `orgReportedRole` Публикация новостей от имени организации.
|
||||||
|
- `orgReaderRole` Подписчик, имеет только права на просмотр информации.
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# Разделы сайта
|
||||||
|
|
||||||
|
## Кабинет участника
|
||||||
|
|
||||||
|
Доступен всем авторизованным пользователям.
|
||||||
|
|
||||||
|
Предназначен для сотрудников организаций, участвующих в конкурсах.
|
||||||
|
|
||||||
|
### Точка входа `/cabinet`
|
||||||
|
|
||||||
|
- `/cabinet/projects` Текущие проекты
|
||||||
|
- `/cabinet/projects/create` Создание нового проекта
|
||||||
|
- `/cabinet/orgs` Список организаций, в которых пользователь является сотрудником
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Управление конкурсом
|
||||||
|
|
||||||
|
Имеют доступ только сотрудники конкурса, `admin` и `support`.
|
||||||
|
|
||||||
|
### Точка входа `/contests`
|
||||||
|
|
||||||
|
- `/contests` выбор конкурса, отображает те конкурсы, в которых пользователь является сотрудником
|
||||||
|
- `/contests/[contestId]` Панель управления конкурсом. Во вложенных страницах можно управлять
|
||||||
|
проектами, заявками, периодами и областями конкурса
|
||||||
|
- `/contests/[contestId]/moderation` Модерация заявок
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Администрирование сайта
|
||||||
|
|
||||||
|
Имеют доступ только `admin` и `support`.
|
||||||
|
|
||||||
|
### Точка входа `/admin`
|
||||||
|
|
||||||
|
- `/admin` Панель администратора
|
||||||
|
|
||||||
|
### Управление пользователями
|
||||||
|
|
||||||
|
- `/admin/users` Список и поиск пользователей
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Общие страницы
|
||||||
|
|
||||||
|
На такие страницы может попасть любой пользователь, если есть разрешение.
|
||||||
|
|
||||||
|
### Авторизация и регистрация
|
||||||
|
|
||||||
|
- `/auth/login` Страница авторизации
|
||||||
|
- `/auth/reg` Страница регистрации
|
||||||
|
- `/profile` Страница профиля текущего пользователя
|
||||||
|
|
||||||
|
### Пользователи
|
||||||
|
|
||||||
|
- `/users/[userId]` Страница отдельного пользователя
|
||||||
|
|
||||||
|
### Проекты
|
||||||
|
|
||||||
|
- `/projects/[projectId]` Просмотр и редактирование проекта
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
label: 'Процесс работы'
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
label: 'Настройка и подключение лендингов'
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Создание конкурса
|
||||||
|
|
||||||
|
Доступно создание конкурса с нуля или путём копирования ранее проведённого конкурса.
|
||||||
|
|
||||||
|
## Создание черновика конкурса
|
||||||
|
|
||||||
|
Создание черновика конкурса требует указания следующих сведений:
|
||||||
|
|
||||||
|
- **Название конкурса**
|
||||||
|
- **Администратор конкурса**
|
||||||
|
|
||||||
|
После создания черновика назначенный администратор получит доступ к панели управления конкурса и к
|
||||||
|
мастеру настройки.
|
||||||
|
|
||||||
|
## Мастер настройки конкурса
|
||||||
|
|
||||||
|
Мастер настройки обеспечивает последовательность действий и визуальный контроль прогресса:
|
||||||
|
|
||||||
|
- **Статус модулей**. Индикация того, какие модули уже настроены, а какие остаются в работе.
|
||||||
|
- **Краткая информация**. Краткое описание каждого модуля и текущий статус настройки.
|
||||||
|
- **Переход к настройке**. Быстрый переход к настройке конкретного модуля.
|
||||||
|
|
||||||
|
> **Важно:** Запуск конкурса становится доступным только после того, как все модули будут отмечены
|
||||||
|
> как "настроенные". После старта мастер настройки завершает работу и закрывается.
|
||||||
|
|
||||||
|
## Варианты создания конкурса
|
||||||
|
|
||||||
|
### Настройка с нуля
|
||||||
|
|
||||||
|
Для некоторых модулей мастер может предоставлять инструменты для быстрого первого заполнения,
|
||||||
|
позволяя задать основные параметры и оставить детали на более поздний этап. Если такие инструменты
|
||||||
|
недоступны, настройка осуществляется стандартными средствами модуля.
|
||||||
|
|
||||||
|
### Копирование существующего конкурса
|
||||||
|
|
||||||
|
Для некоторых модулей мастер может предложить выбор элементов для переноса из ранее проведённого
|
||||||
|
конкурса. Это ускоряет первоначальную конфигурацию. Если инструмент копирования недоступен,
|
||||||
|
применяются обычные средства модуля для настройки существующего конкурса.
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# Категории проектов в конкурсах
|
||||||
|
|
||||||
|
Категории позволяют учитывать специфику проектов и их участников. Пользователи сами выбирают
|
||||||
|
категорию, в которой хотят участвовать и заполняют соответствующую форму заявки.
|
||||||
|
|
||||||
|
#### Примеры категорий и их отличий:
|
||||||
|
|
||||||
|
- **Волонтерские проекты:** Ограничения по срокам проведения, максимальная сумма грантов,
|
||||||
|
упрощенные формы заявки.
|
||||||
|
|
||||||
|
- **Школьные проекты:** Участниками могут быть только группы школьников, дополнительные требования
|
||||||
|
для школ.
|
||||||
|
|
||||||
|
- **Проекты организаций:** Более сложные формы заявки, большие максимальные суммы грантов.
|
||||||
|
|
||||||
|
### Функционал категорий
|
||||||
|
|
||||||
|
- **Создание и управление категориями:**
|
||||||
|
|
||||||
|
- Только администраторы конкурса могут управлять категориями.
|
||||||
|
- Администраторы могут добавлять, изменять и удалять категории.
|
||||||
|
|
||||||
|
- **Параметры категории:**
|
||||||
|
|
||||||
|
- Условия участия (например, ограничения по типу участников, возрасту, региону).
|
||||||
|
- Формы заявки (разделы, поля, инструкции).
|
||||||
|
- Параметры конкурсов (сроки подачи, суммы грантов).
|
||||||
|
- И так далее.
|
||||||
|
|
||||||
|
- **Гибкость изменения:** Возможность адаптации категорий для конкретных конкурсов.
|
||||||
|
|
||||||
|
### Технические особенности
|
||||||
|
|
||||||
|
- У категорий нет версионирования.
|
||||||
|
- Изменять категории можно в любой момент до завершения конкурса.
|
||||||
|
- Категория относится только к одному конкурсу.
|
||||||
|
- Участники должны видеть только public категории.
|
||||||
|
- Участник не может сменить категории после ее выбора.
|
||||||
|
- Проект может относиться только к одной категории.
|
||||||
|
|
||||||
|
### Отчетность по категориям
|
||||||
|
|
||||||
|
В разделе аналитики предоставляется возможность фильтровать заявки и результаты по категориям,
|
||||||
|
анализировать успешность проектов в каждой категории и их соответствие целям конкурсов.
|
||||||
|
|
||||||
|
### Доступность категорий
|
||||||
|
|
||||||
|
Для управления доступностью категории для создания проектов с этой категорией используется поле
|
||||||
|
`access`.
|
||||||
|
|
||||||
|
Любую категорию можно включить или выключить для создания нового проекта с этой категорией.
|
||||||
|
|
||||||
|
- `enabled: true` Участники могут создавать проекты в этой категории.
|
||||||
|
- `enabled: false` Участники не могут создавать проекты в этой категории.
|
||||||
|
|
||||||
|
Флаг `manualControl` управляет способом изменения поля `enabled`:
|
||||||
|
|
||||||
|
- `true` - включается и выключается только вручную.
|
||||||
|
- `false` - включается и выключается автоматически, на основании других настроек категории.
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Функционал категорий
|
||||||
|
|
||||||
|
Категории предоставляют гибкость настройки условий для разных типов проектов. Каждая категория может
|
||||||
|
изменять следующие параметры:
|
||||||
|
|
||||||
|
- Максимальная сумма гранта
|
||||||
|
- Максимальный фонд гранта
|
||||||
|
- Условия участия (текст и документ)
|
||||||
|
- Требования к участникам (текст и документ)
|
||||||
|
- Сроки подачи заявок для данной категории
|
||||||
|
- Направления проектов
|
||||||
|
- Форма заявки
|
||||||
|
- Список документов, которые могут потребоваться участникам от организатора конкурса
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
label: 'Категории проектов'
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
label: 'Конкурсы'
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Общая схема взаимодействия модулей
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
erDiagram
|
||||||
|
Contest ||--o{ Project : "Проект участвует в конкурсе"
|
||||||
|
|
||||||
|
Organizator ||--o{ Contest : "Организатор может создать конкурс"
|
||||||
|
Organizator ||--o{ Project : "Организатор может создать проект"
|
||||||
|
|
||||||
|
Project ||--o{ Event: "Мероприятие проходит в рамках проекта"
|
||||||
|
Project ||--o{ News: "У проекта есть новости"
|
||||||
|
Project ||--o{ Application: "Данные проекта изменяются через заявки"
|
||||||
|
```
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
label: 'Организации и волонтеры'
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Организаторы проектов
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class Organizator {
|
||||||
|
<<interface>>
|
||||||
|
id: pk
|
||||||
|
type: "Organization" | "Volunteers"
|
||||||
|
title: string
|
||||||
|
leader: OrgParticipant
|
||||||
|
}
|
||||||
|
|
||||||
|
class Organization {
|
||||||
|
type: "Organization"
|
||||||
|
urData: UrData
|
||||||
|
recvezits: any
|
||||||
|
}
|
||||||
|
Organization --|> Organizator
|
||||||
|
|
||||||
|
class Volunteers {
|
||||||
|
type: "Volunteers"
|
||||||
|
}
|
||||||
|
Volunteers --|> Organizator
|
||||||
|
```
|
||||||
|
|
||||||
|
- В случае волонтеров названием организатора будет являться ФИО руководителя (предварительно)
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Общая последовательность работы над проектом
|
||||||
|
|
||||||
|
- Любое изменение проекта проходит через создание заявки на изменение проекта
|
||||||
|
- Изменение в проект вносится только после одобрения заявки модератором
|
||||||
|
- При создании нового проекта, проект создается со статусом `Draft`, без создания заявки
|
||||||
|
- Проект начинает участвовать в конкурсе только после одобрения заявки модератором
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
actor User as Пользователь
|
||||||
|
participant Application
|
||||||
|
actor Moderation as Модератор
|
||||||
|
participant Project
|
||||||
|
|
||||||
|
Note over User, Project: Создание проекта
|
||||||
|
|
||||||
|
User->>+Project: Создание черновика проекта
|
||||||
|
|
||||||
|
Note over User, Project: Заявка на участие в конкурсе
|
||||||
|
|
||||||
|
User->>Application: Заполнение заявки
|
||||||
|
Application->>Moderation: Отправка на модерацию
|
||||||
|
Moderation-->>Application: Возврат на доработку
|
||||||
|
User-->>Application: Исправление заявки
|
||||||
|
Application-->>Moderation: Повторная оправка на модерацию
|
||||||
|
Moderation->>+Project: Проект учавствует в конкурсе
|
||||||
|
|
||||||
|
Note over User, Project: Проект профинансирован
|
||||||
|
|
||||||
|
loop
|
||||||
|
Note over User, Project: Заявка на изменение проекта
|
||||||
|
User->>Application: Заполнение заявки
|
||||||
|
Application->>Moderation: Отправка на модерацию
|
||||||
|
Moderation-->>Application: Возврат на доработку
|
||||||
|
User-->>Application: Исправление заявки
|
||||||
|
Application-->>Moderation: Повторная оправка на модерацию
|
||||||
|
Moderation->>+Project: Применение изменений на проект
|
||||||
|
end
|
||||||
|
|
||||||
|
User->Project: Завершение работы над проектом
|
||||||
|
|
||||||
|
deactivate Project
|
||||||
|
deactivate Project
|
||||||
|
deactivate Project
|
||||||
|
```
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# Спецификация
|
||||||
|
|
||||||
|
## Исходные требования
|
||||||
|
|
||||||
|
- Все изменения в проект (не черновик) вносятся только после прохождения модерации
|
||||||
|
- Вся история изменения проекта должна храниться столько же, сколько и сам проект
|
||||||
|
|
||||||
|
## Проект
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> Draft
|
||||||
|
Draft --> Rejected : Заявка отклонена модераторами
|
||||||
|
Draft --> Accepted : Заявка одобрена модераторами
|
||||||
|
Accepted --> Evaluating
|
||||||
|
Evaluating --> Awarding
|
||||||
|
|
||||||
|
note left of Evaluating
|
||||||
|
Оценка проекта
|
||||||
|
end note
|
||||||
|
|
||||||
|
Awarding --> Finalists : Проект не получил финансирование
|
||||||
|
Awarding --> Funded : Проект победил
|
||||||
|
|
||||||
|
note left of Awarding
|
||||||
|
Выбор победителей
|
||||||
|
end note
|
||||||
|
|
||||||
|
Rejected --> [*]
|
||||||
|
Finalists --> [*]
|
||||||
|
Funded --> [*]
|
||||||
|
```
|
||||||
|
|
||||||
|
- Проект со статусами `Draft` и `Rejected` не участвует в конкурсе
|
||||||
|
- Проект со статусами `Draft` и `Rejected` не видны никому, кроме участников организации и
|
||||||
|
модераторов
|
||||||
|
- Проект с остальными статусами участвует в конкурсе
|
||||||
|
- Отклоненный проект не участвует в конкурсе и не может быть восстановлен, только создан новый
|
||||||
|
проект
|
||||||
|
- Проект может быть удален авторами в любой любой момент до завершения конкурса без указания
|
||||||
|
причины, за исключением статуса `Funded`, т.к. этот проект уже направляется на финансирование
|
||||||
|
(не показано на схеме)
|
||||||
|
- Проект может быть отклонен организаторами конкурса в любой любой момент до завершения конкурса с
|
||||||
|
указанием причины отклонения (не показано на схеме, это техническая возможность, для этого
|
||||||
|
должны быть веские основания)
|
||||||
|
|
||||||
|
## Заявка
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> Draft
|
||||||
|
|
||||||
|
Draft --> Moderating: Отправка на модерацию
|
||||||
|
|
||||||
|
Moderating --> Rejected: Отклонена модераторами
|
||||||
|
Rejected --> [*]
|
||||||
|
|
||||||
|
Moderating --> Accepted: Одобрена модераторами
|
||||||
|
Accepted --> [*]
|
||||||
|
|
||||||
|
Moderating --> Returned: Возврат на доработку
|
||||||
|
Returned --> Draft: Исправление заявки
|
||||||
|
|
||||||
|
Draft --> Deleted: Удаление заявки автором
|
||||||
|
Deleted --> [*]
|
||||||
|
```
|
||||||
|
|
||||||
|
- У проекта одновременно может быть несколько заявок в работе
|
||||||
|
- У проекта со статусом НЕ `Draft` обязательно должна быть заявка со статусом `Accepted`, притом
|
||||||
|
только одна
|
||||||
|
- Заявка со статусами `Draft`, `Rejected`, `Deleted` не видна никому, кроме участников организации
|
||||||
|
и модераторов
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Ревизии заявки
|
||||||
|
|
||||||
|
Сама ревизия хранит только отличия от предыдущей ревизии, кто и когда внес изменения. При
|
||||||
|
необходимости можно просмотреть историю изменений заявки и откатиться к предыдущим версиям.
|
||||||
|
|
||||||
|
## Исходные требования
|
||||||
|
|
||||||
|
- Все изменения изменения заявки должны быть сохранены
|
||||||
|
- Исключить потерю данных при редактировании заявки несколькими пользователями
|
||||||
|
- Возможность отката к предыдущим версиям заявки
|
||||||
|
- Возможность просмотра истории изменений заявки
|
||||||
|
- Возможность формирования графика интенсивности работы над заявкой
|
||||||
|
|
||||||
|
## Реализация
|
||||||
|
|
||||||
|
Над заявкой могут производится различные действия, которые влияют на ее состояние и данные. При этом
|
||||||
|
разные действия могут вносить разные изменения в данные заявки. Потому в ревизии есть отдельные поля
|
||||||
|
`action` и `payloadType`, которые позволяют определить тип действия и тип изменений в данных заявки.
|
||||||
|
|
||||||
|
Тип данных в поле `payload` зависит от значения поля `payloadType` и должно обрабатываться
|
||||||
|
соответствующим образом.
|
||||||
|
|
||||||
|
Такая структура позволяет легко добавлять новые типы действий и изменений в заявке.
|
||||||
|
|
||||||
|
## Заявка
|
||||||
|
|
||||||
|
Для получения актуальной версии заявки необходимо применить все ревизии в порядке их создания.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
label: 'Проекты и заявки'
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
label: 'Заявки (Requests)'
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# Участники проектов
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class User {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
class Contest {
|
||||||
|
id: pk
|
||||||
|
status: ContestStatus
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
class Organizator {
|
||||||
|
<<interface>>
|
||||||
|
id: pk
|
||||||
|
type: Organization | Volunteers
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
class Project {
|
||||||
|
id: pk
|
||||||
|
title: string
|
||||||
|
contest: Contest
|
||||||
|
org: Organizator
|
||||||
|
}
|
||||||
|
Project "*" --* "1" Organizator
|
||||||
|
Project "*" --* "1" Contest
|
||||||
|
|
||||||
|
class ContestMember {
|
||||||
|
id: pk
|
||||||
|
contest: Contest
|
||||||
|
role: ContestRole
|
||||||
|
roleTitle: string
|
||||||
|
}
|
||||||
|
ContestMember "*" --* "1" Contest
|
||||||
|
ContestMember "*" --o "1" User
|
||||||
|
|
||||||
|
class OrgMember {
|
||||||
|
id: pk
|
||||||
|
org: Organizator
|
||||||
|
role: OrgRole
|
||||||
|
roleTitle: string
|
||||||
|
}
|
||||||
|
OrgMember "*" --* "1" Organizator
|
||||||
|
OrgMember "*" --o "1" User
|
||||||
|
|
||||||
|
class ProjectMember {
|
||||||
|
id: pk
|
||||||
|
project: Project
|
||||||
|
role: ProjectRole
|
||||||
|
roleTitle: string
|
||||||
|
}
|
||||||
|
ProjectMember "*" --* "1" Project
|
||||||
|
ProjectMember "*" --o "1" User
|
||||||
|
|
||||||
|
class Participant {
|
||||||
|
id: pk
|
||||||
|
fio
|
||||||
|
phone
|
||||||
|
birthYear
|
||||||
|
user?: User
|
||||||
|
}
|
||||||
|
Participant "1" --o "0..1" User
|
||||||
|
|
||||||
|
class OrgParticipant {
|
||||||
|
id: pk
|
||||||
|
org: Organizator
|
||||||
|
participant: Participant
|
||||||
|
roleTitle: string
|
||||||
|
}
|
||||||
|
OrgParticipant "*" --* "1" Organizator
|
||||||
|
OrgParticipant "*" --* "1" Participant
|
||||||
|
|
||||||
|
class ProjectParticipant {
|
||||||
|
id: pk
|
||||||
|
project: Project
|
||||||
|
participant: Participant
|
||||||
|
roleTitle: string
|
||||||
|
}
|
||||||
|
ProjectParticipant "*" --* "1" Project
|
||||||
|
ProjectParticipant "*" --* "1" Participant
|
||||||
|
```
|
||||||
|
|
||||||
|
- Заявка превращается в конкурс (предположительно) после апрува модератором
|
||||||
|
|
||||||
|
### Различные типы участников
|
||||||
|
|
||||||
|
| Поле | Имя | Ключевые (руководитель, бухгалтер) | Участник проекта организации | Участник проекта волонтеров |
|
||||||
|
| ------------------------------ | -------------- | ---------------------------------- | ---------------------------- | --------------------------- |
|
||||||
|
| Фамилия | firstName | + | + | + |
|
||||||
|
| Имя | lastName | + | + | + |
|
||||||
|
| Отчество | patronymic | + | + | + |
|
||||||
|
| Должность | position | + | + | - |
|
||||||
|
| Телефон | phone | + | - | + |
|
||||||
|
| E-mail | email | + | - | + |
|
||||||
|
| Год рождения | birthYear | - | + | - |
|
||||||
|
| Зона ответственности в проекте | responsibility | - | + | - |
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# Конкурсы, проекты и заявки
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
direction BT
|
||||||
|
class Organizator {
|
||||||
|
<<interface>>
|
||||||
|
id: pk
|
||||||
|
type: Organization | Volunteers
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
class Contest {
|
||||||
|
id: pk
|
||||||
|
status: ContestStatus
|
||||||
|
title: string
|
||||||
|
logo: string
|
||||||
|
description: string
|
||||||
|
totalBudget: number
|
||||||
|
receiptStartedAt: number
|
||||||
|
receiptEndedAt: number
|
||||||
|
ratingStartedAt?: number
|
||||||
|
ratingEndedAt?: number
|
||||||
|
publicResultsAt: number
|
||||||
|
workStartedAt: number
|
||||||
|
workEndedAt: number
|
||||||
|
createdAt: number
|
||||||
|
updatedAt: number
|
||||||
|
org: Organizator
|
||||||
|
}
|
||||||
|
Contest "*" --* "1" Organizator
|
||||||
|
|
||||||
|
class Activity {
|
||||||
|
id: pk
|
||||||
|
title: string
|
||||||
|
parent?: Activity
|
||||||
|
}
|
||||||
|
Activity "*" --* "1" Contest
|
||||||
|
Activity "1" --* "0..1" Activity
|
||||||
|
|
||||||
|
class Project {
|
||||||
|
id: pk
|
||||||
|
title: string
|
||||||
|
contest: Contest
|
||||||
|
activity: Activity
|
||||||
|
org: Organizator
|
||||||
|
leader: ProjectParticipant
|
||||||
|
description: string
|
||||||
|
}
|
||||||
|
Project "*" --* "1" Organizator
|
||||||
|
Project "*" --* "1" Contest
|
||||||
|
Project "*" --o "1" Activity
|
||||||
|
|
||||||
|
class OrgParticipant {
|
||||||
|
id: pk
|
||||||
|
org: Organizator
|
||||||
|
fio
|
||||||
|
roleTitle: string
|
||||||
|
}
|
||||||
|
OrgParticipant "*" --* "1" Organizator
|
||||||
|
|
||||||
|
class ProjectParticipant {
|
||||||
|
id: pk
|
||||||
|
project: Project
|
||||||
|
fio
|
||||||
|
roleTitle: string
|
||||||
|
}
|
||||||
|
ProjectParticipant "*" --* "1" Project
|
||||||
|
|
||||||
|
class Event {
|
||||||
|
id: pk
|
||||||
|
status: EventStatus
|
||||||
|
project: Project
|
||||||
|
title: string
|
||||||
|
logo
|
||||||
|
description: string
|
||||||
|
startsOn: Date
|
||||||
|
endsOn: Date
|
||||||
|
createdAt: timestamp
|
||||||
|
updatedAt: timestamp
|
||||||
|
}
|
||||||
|
Event "*" --* "1" Project
|
||||||
|
|
||||||
|
class News {
|
||||||
|
id
|
||||||
|
status: EventStatus
|
||||||
|
project: Project
|
||||||
|
title: string
|
||||||
|
logo
|
||||||
|
description: string
|
||||||
|
date: Date
|
||||||
|
createdAt: timestamp
|
||||||
|
updatedAt: timestamp
|
||||||
|
}
|
||||||
|
News "*" --* "1" Project
|
||||||
|
```
|
||||||
|
|
||||||
|
- Заявка превращается в конкурс (предположительно) после апрува модератором
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Заявки
|
||||||
|
|
||||||
|
## Состояния заявки
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> Draft
|
||||||
|
Draft --> Autosave
|
||||||
|
Autosave --> Draft: Обновление черновика
|
||||||
|
Draft --> Moderating: Отправлено на модерацию
|
||||||
|
Moderating --> Returned: на доработку
|
||||||
|
Returned --> Draft: исправление
|
||||||
|
Moderating --> Rejected: заявка отклонена
|
||||||
|
Moderating --> Accepted: заявка принята
|
||||||
|
Accepted --> Archived: обновление заявки принято
|
||||||
|
Rejected --> [*]
|
||||||
|
```
|
||||||
|
|
||||||
|
- `Autosave` отдельная запись с указанием на черновик, при сохранении обновляет черновик
|
||||||
|
- `Accepted` и `Archived` обязаны иметь верное значение `projectId` т.к. при принятии заявки
|
||||||
|
создается проект и дальнейшие действия ведутся над проектом
|
||||||
|
- Заявки в статусе отличном от `Accepted` и `Archived` могут иметь `projectId` только если это
|
||||||
|
заявка на обновление проекта
|
||||||
|
- Отклоненная заявка не может быть подана повторно
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Введение
|
||||||
|
|
||||||
|
Данный модуль обеспечивает экспертную оценку заявок. Он автоматизирует распределение заявок между
|
||||||
|
экспертами, сбор и анализ оценок, а также формирование итогового рейтинга проектов.
|
||||||
|
|
||||||
|
## Общий процесс оценки проектов
|
||||||
|
|
||||||
|
1. Организатор настраивает форму оценки и критерии.
|
||||||
|
2. Система назначает экспертов на проекты.
|
||||||
|
3. Эксперты заполняют форму оценки.
|
||||||
|
4. Итоговые оценки агрегируются для формирования рейтинга.
|
||||||
|
|
||||||
|
## Роли участников
|
||||||
|
|
||||||
|
- **Организатор конкурса** – настраивает критерии, назначает экспертов, контролирует процесс.
|
||||||
|
- **Эксперт** – оценивает проекты по заданным критериям.
|
||||||
|
- **Платформа** – автоматически распределяет проекты, фиксирует оценки, собирает данные.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Создание формы оценки
|
||||||
|
|
||||||
|
## Назначение и общие принципы
|
||||||
|
|
||||||
|
Так как эксперты оценивают проекты по заранее настроенным критериям, требуется конструктор формы,
|
||||||
|
которую будут заполнять эксперты. Организатор конкурса настраивает критерии оценки до начала
|
||||||
|
конкурса. После начала оценки структура формы не может быть изменена.
|
||||||
|
|
||||||
|
## Типы критериев
|
||||||
|
|
||||||
|
- **Выбор из фиксированного списка ответов** (каждый вариант имеет скрытый для эксперта вес).
|
||||||
|
- **Текстовое поле** с настройками ограничений по длине.
|
||||||
|
|
||||||
|
Так же критерию можно добавить описание и подсказку.
|
||||||
|
|
||||||
|
## Группировка критериев
|
||||||
|
|
||||||
|
- Критерии разделены на группы.
|
||||||
|
- Некоторые критерии могут быть необязательными.
|
||||||
|
- Если для группы критериев требуется комментарий, то его нужно добавить в схему.
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Назначение экспертов
|
||||||
|
|
||||||
|
## Принципы назначения
|
||||||
|
|
||||||
|
Платформа для грантов должна распределять проекты среди экспертов для независимой оценки. Процесс
|
||||||
|
назначения проектов должен учитывать:
|
||||||
|
|
||||||
|
- Автоматическое и ручное распределение.
|
||||||
|
- Обеспечение равномерной нагрузки на экспертов.
|
||||||
|
- Возможность перераспределения проектов в случае отказа эксперта.
|
||||||
|
|
||||||
|
## Ручное назначение
|
||||||
|
|
||||||
|
Организатор может вручную назначать экспертов и корректировать автоматическое распределение.
|
||||||
|
|
||||||
|
## Автоматическое назначение
|
||||||
|
|
||||||
|
Система может:
|
||||||
|
|
||||||
|
- Находить и назначать на проект наименее загруженного эксперта.
|
||||||
|
- Балансировать указанный проект.
|
||||||
|
- Балансировать все проекты категории.
|
||||||
|
|
||||||
|
Балансировать проект - доназначать экспертов до нужного количества, если сейчас их меньше, чем надо.
|
||||||
|
|
||||||
|
Обработка ошибок:
|
||||||
|
|
||||||
|
- Если найти и назначить наименее загруженного **эксперта** не удаётся, пользователю предлагается
|
||||||
|
**назначить его вручную**.
|
||||||
|
- Если для балансировки **проекта** не хватает экспертов, операция **отменяется**.
|
||||||
|
- Если для балансировки какого-то из проектов **категории** не хватает экспертов, этот **проект
|
||||||
|
помечается** `unassessable` и балансировка категории продолжается.
|
||||||
|
|
||||||
|
## Обработка отказов
|
||||||
|
|
||||||
|
Если эксперт отказывается от оценки, система автоматически переназначает проект другому эксперту.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Оценка проектов экспертами
|
||||||
|
|
||||||
|
## Процесс оценки
|
||||||
|
|
||||||
|
- Эксперт оценивает проект независимо, не видя оценок других экспертов.
|
||||||
|
- Черновики сохраняются автоматически.
|
||||||
|
- Эксперт может редактировать оценку, пока она не отправлена.
|
||||||
|
- После отправки оценку изменить нельзя.
|
||||||
|
|
||||||
|
## Форма
|
||||||
|
|
||||||
|
- Обязательное заполнение всех критериев.
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# Анализ ревью
|
||||||
|
|
||||||
|
После того как эксперты отправляют свои оценки, система автоматически анализирует их, вычисляет
|
||||||
|
средний балл проекта и определяет, завершена ли экспертиза.
|
||||||
|
|
||||||
|
Это происходит при каждом изменении ревью: при завершении, отклонении или отказе от ревью.
|
||||||
|
|
||||||
|
## Средний балл
|
||||||
|
|
||||||
|
Средний балл проекта рассчитывается как среднее арифметическое баллов всех завершённых ревью,
|
||||||
|
округлённое до целого. Незавершённые, отклонённые и удалённые ревью в расчёте не участвуют. Если ни
|
||||||
|
одно ревью ещё не завершено, средний балл не отображается.
|
||||||
|
|
||||||
|
## Завершение экспертизы
|
||||||
|
|
||||||
|
Экспертиза проекта считается завершённой, когда выполнены два условия одновременно:
|
||||||
|
|
||||||
|
- Все назначенные эксперты завершили свои ревью (не считая отклонённых и удалённых).
|
||||||
|
- Количество завершённых ревью не меньше минимально необходимого количества, заданного в
|
||||||
|
настройках номинации.
|
||||||
|
|
||||||
|
## Спорные оценки
|
||||||
|
|
||||||
|
Когда эксперты расходятся во мнениях слишком сильно, система помечает проект как спорный. Это
|
||||||
|
позволяет организатору обратить внимание на такие проекты и при необходимости назначить
|
||||||
|
дополнительных экспертов.
|
||||||
|
|
||||||
|
### Как определяется спорность
|
||||||
|
|
||||||
|
Проверка спорности происходит, когда набрано ровно минимально необходимое количество ревью. Система
|
||||||
|
сравнивает разброс оценок — разницу между максимальным и минимальным баллом среди всех завершённых
|
||||||
|
ревью — с допустимым порогом, заданным в настройках номинации в процентах от шкалы оценки.
|
||||||
|
|
||||||
|
- Если разброс превышает порог — проект помечается как спорный и экспертиза не завершается, чтобы
|
||||||
|
организатор мог назначить дополнительного эксперта.
|
||||||
|
- Если разброс в пределах порога — проект не спорный, экспертиза завершается.
|
||||||
|
|
||||||
|
### Дополнительные ревью
|
||||||
|
|
||||||
|
Когда организатор назначает дополнительного эксперта на спорный проект и тот завершает ревью,
|
||||||
|
количество ревью превышает минимально необходимое. В этом случае:
|
||||||
|
|
||||||
|
- Проект остаётся помеченным как спорный (метка не снимается автоматически).
|
||||||
|
- Экспертиза завершается, когда все назначенные эксперты завершили свои ревью.
|
||||||
|
- Модератор должен вручную снять метку спорности, если считает, что дополнительное ревью разрешило
|
||||||
|
спор.
|
||||||
|
|
||||||
|
### При недостатке ревью
|
||||||
|
|
||||||
|
Если количество завершённых ревью меньше минимально необходимого (например, ревью было отклонено),
|
||||||
|
метка спорности снимается и экспертиза остаётся незавершённой.
|
||||||
|
|
||||||
|
## Пересчёт проектов всей номинации
|
||||||
|
|
||||||
|
При изменении настроек оценки в номинации запускается пересчёт всех проектов в номинации.
|
||||||
|
|
||||||
|
При пересчёте:
|
||||||
|
|
||||||
|
- Затрагиваются только проекты с незавершённой или завершённой экспертизой.
|
||||||
|
- Метка спорности сбрасывается у всех проектов и вычисляется заново.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
label: 'Оценка проектов'
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
label: 'Отчетность'
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Отчетность
|
||||||
|
|
||||||
|
Пользователи, получившие гранты должны предоставить отчеты по расходу полученных средств
|
||||||
|
|
||||||
|
Отчетности всего две
|
||||||
|
|
||||||
|
## Финансовый отчет
|
||||||
|
|
||||||
|
Сколько было потрачено средств, на что, с комментариями и прикреплением документов
|
||||||
|
|
||||||
|
## Аналитический отчет
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
label: 'Продуктовые модули'
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
label: 'Гайдлайны'
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
# Модальное окно
|
||||||
|
|
||||||
|
Модальное окно — диалоговый элемент интерфейса, который появляется поверх страницы и блокирует
|
||||||
|
доступ к её основному содержимому.
|
||||||
|
|
||||||
|
## Когда использовать
|
||||||
|
|
||||||
|
Используйте модальные окна для:
|
||||||
|
|
||||||
|
- подтверждения действий,
|
||||||
|
- отображения ошибок,
|
||||||
|
- вывода небольших форм (до 10 полей), связанных с локальными действиями — например, настройками
|
||||||
|
или созданием объекта.
|
||||||
|
|
||||||
|
Не используйте модальные окна для больших форм (> 15 полей) и действий, требующих длительного
|
||||||
|
непрерывного взаимодействия пользователя с интерфейсом.
|
||||||
|
|
||||||
|
## Принцип работы
|
||||||
|
|
||||||
|
Модальное окно отображается поверх страницы с затемнением фона. Это помогает сфокусировать внимание
|
||||||
|
пользователя на локальном действии, сохранив контекст.
|
||||||
|
|
||||||
|
Модальное окно:
|
||||||
|
|
||||||
|
- перехватывает фокус внутри себя,
|
||||||
|
- восстанавливает фокус после закрытия (свойство `restoreFocus`).
|
||||||
|
|
||||||
|
## Состав модального окна
|
||||||
|
|
||||||
|
Модальное окно может включать:
|
||||||
|
|
||||||
|
- **заголовок** (обязателен),
|
||||||
|
- **кнопку закрытия**,
|
||||||
|
- **иконку статуса**,
|
||||||
|
- **контент**,
|
||||||
|
- **подвал с действиями**.
|
||||||
|
|
||||||
|
## Компоненты модальных окон
|
||||||
|
|
||||||
|
### `NoticeDialog`
|
||||||
|
|
||||||
|
Используется для отображения информационных сообщений:
|
||||||
|
|
||||||
|
- успешные действия,
|
||||||
|
- ошибки,
|
||||||
|
- предупреждения,
|
||||||
|
- служебные уведомления.
|
||||||
|
|
||||||
|
Особенности:
|
||||||
|
|
||||||
|
- одна кнопка действия («ОК» или «Закрыть»),
|
||||||
|
- иконка и цвет определяют тип сообщения,
|
||||||
|
- контент — опционален,
|
||||||
|
- кнопка получает фокус.
|
||||||
|
|
||||||
|
Типы сообщений:
|
||||||
|
|
||||||
|
- успех — зелёное оформление `variant=success` и позитивная иконка,
|
||||||
|
- ошибка — красное оформление `variant=danger` и иконка ошибки,
|
||||||
|
- предупреждение — жёлтое оформление `variant=warning` и иконка предупреждения,
|
||||||
|
- информация — синее оформление `variant=info` и информационная иконка.
|
||||||
|
|
||||||
|
### `ActionDialog`
|
||||||
|
|
||||||
|
Используется для модальных окон, которые последовательно проходят следующие состояния:
|
||||||
|
|
||||||
|
- подтверждение действия,
|
||||||
|
- выполнение асинхронной операции,
|
||||||
|
- отображение статуса выполнения,
|
||||||
|
- сообщение об успехе или ошибке.
|
||||||
|
|
||||||
|
Особенности:
|
||||||
|
|
||||||
|
- объединяет несколько состояний в одном окне,
|
||||||
|
- обеспечивает единый UX-паттерн для типовых async-сценариев,
|
||||||
|
- используется вместо самостоятельной реализации поведения, если сценарий вписывается в типовой
|
||||||
|
flow.
|
||||||
|
|
||||||
|
### `Dialog`
|
||||||
|
|
||||||
|
Базовый компонент модального окна. Предназначен для:
|
||||||
|
|
||||||
|
- подтверждения действий,
|
||||||
|
- ввода дополнительной информации,
|
||||||
|
- отображения сценариев, не охваченных `NoticeDialog` и `ActionDialog`.
|
||||||
|
|
||||||
|
Особенности:
|
||||||
|
|
||||||
|
- может содержать любое количество кнопок,
|
||||||
|
- может быть гибко настроен под различные сценарии,
|
||||||
|
- требует самостоятельной реализации логики поведения.
|
||||||
|
|
||||||
|
## Диалоговые окна
|
||||||
|
|
||||||
|
### Заголовок
|
||||||
|
|
||||||
|
Заголовок должен быть кратким (1–3 слова) и отражать суть действия или процесса:
|
||||||
|
|
||||||
|
- Создание проекта
|
||||||
|
- Редактирование события
|
||||||
|
|
||||||
|
Для окон, требующих подтверждения:
|
||||||
|
|
||||||
|
- Удалить проект?
|
||||||
|
- Выйти без сохранения?
|
||||||
|
|
||||||
|
### Действия
|
||||||
|
|
||||||
|
В диалоговых окнах обычно две кнопки:
|
||||||
|
|
||||||
|
- **Основная кнопка** — подтверждает действие («Сохранить», «Удалить»). Основная кнопка должна
|
||||||
|
быть в фокусе.
|
||||||
|
- **Кнопка отмены** — закрывает окно без выполнения действия («Отменить»).
|
||||||
|
|
||||||
|
> **Правило:** чем правее кнопка — тем менее важное и менее частое действие. Вот обновлённая краткая
|
||||||
|
> версия с переносом правил подтверждения в конец:
|
||||||
|
|
||||||
|
## Именование компонентов
|
||||||
|
|
||||||
|
Для модальных окон используется единый принцип именования, который отражает тип действия и
|
||||||
|
обеспечивает предсказуемость.
|
||||||
|
|
||||||
|
### Обычные модальные окна
|
||||||
|
|
||||||
|
Используется паттерн:
|
||||||
|
|
||||||
|
> **`<Entity><Action>Modal`**
|
||||||
|
|
||||||
|
Применяется для создания, редактирования, просмотра или выполнения безопасных действий.
|
||||||
|
|
||||||
|
Где:
|
||||||
|
|
||||||
|
- **Entity** — сущность: `Project`, `Review`, `Expert`, …
|
||||||
|
- **Action** — действие в глагольной форме: `Create`, `Edit`, `Assign`, `View`, …
|
||||||
|
|
||||||
|
Примеры:
|
||||||
|
|
||||||
|
- `ProjectCreateModal`
|
||||||
|
- `CategoryEditModal`
|
||||||
|
- `ReviewDetailsModal`
|
||||||
|
- `ExpertAssignModal`
|
||||||
|
|
||||||
|
### Подтверждение действий
|
||||||
|
|
||||||
|
Для модальных окон, которые подтверждают действие, добавляется префикс:
|
||||||
|
|
||||||
|
> **`<Entity><Action>ConfirmModal`**
|
||||||
|
|
||||||
|
Примеры:
|
||||||
|
|
||||||
|
- `ProjectDeleteConfirmModal`
|
||||||
|
- `ExpertUnassignConfirmModal`
|
||||||
|
- `ReviewRejectConfirmModal`
|
||||||
|
|
||||||
|
### Преимущества схемы
|
||||||
|
|
||||||
|
- структурированность,
|
||||||
|
- предсказуемость,
|
||||||
|
- понятные правила,
|
||||||
|
- единообразие в кодовой базе.
|
||||||
|
|
||||||
|
## Закрытие модального окна
|
||||||
|
|
||||||
|
Клик по иконке закрытия, нажатие _Esc_ или клик вне модального окна — **эквивалентные действия**,
|
||||||
|
приводящие к отмене и закрытию.
|
||||||
|
|
||||||
|
Исключения:
|
||||||
|
|
||||||
|
- важные процессы, которые нельзя прервать случайно;
|
||||||
|
- формы ввода данных, во избежание потери изменений, если эти изменения критичны.
|
||||||
|
|
||||||
|
В таких случаях:
|
||||||
|
|
||||||
|
- окно может не закрываться через Esc/клик вне области;
|
||||||
|
- перед закрытием нужно уточнить: сохранить изменения или выйти.
|
||||||
|
|
||||||
|
## Асинхронные действия
|
||||||
|
|
||||||
|
Используйте `ActionDialog`, если:
|
||||||
|
|
||||||
|
- нужно подтвердить действие,
|
||||||
|
- выполнить асинхронную операцию,
|
||||||
|
- показать прогресс,
|
||||||
|
- отобразить успех или ошибку.
|
||||||
|
|
||||||
|
Для особых сценариев допускается использовать обычный `Dialog`, но:
|
||||||
|
|
||||||
|
- во время выполнения действия кнопки должны быть заблокированы,
|
||||||
|
- окно не должно закрываться до завершения операции,
|
||||||
|
- ошибка должна быть отображена в понятной форме.
|
||||||
|
|
||||||
|
## Dialog vs Modal
|
||||||
|
|
||||||
|
> Кратко: **Dialog — технология, Modal — сценарий продукта.**
|
||||||
|
|
||||||
|
- **`Dialog`** используется только для инфраструктурных UI-компонентов дизайн-системы (например,
|
||||||
|
`Dialog`, `ActionDialog`, `NoticeDialog`). Это строительные блоки, не связанные с доменными
|
||||||
|
сущностями.
|
||||||
|
|
||||||
|
- **`Modal`** используется для прикладных модальных окон, связанных с реальными пользовательскими
|
||||||
|
сценариями (`ProjectCreateModal`, `ConfirmProjectDeleteModal`, `ReviewDetailsModal`). Такие
|
||||||
|
компоненты используют `Dialog` внутри, но представляют собой доменные элементы интерфейса.
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# Быстрый старт
|
||||||
|
|
||||||
|
## Репозиторий
|
||||||
|
|
||||||
|
Настраиваем [ssh ключи в gitea](https://git.jt4d.ru/user/settings/keys), клонируем репозиторий и
|
||||||
|
ставим зависимости. В проекте используется пакетный менеджер yarn
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone ssh://git@git.jt4d.ru:2222/1vit/more.git
|
||||||
|
cd more
|
||||||
|
yarn
|
||||||
|
```
|
||||||
|
|
||||||
|
Установка может занять несколько минут.
|
||||||
|
|
||||||
|
## Настройка БД
|
||||||
|
|
||||||
|
Рекомендуется устанавливать всё через docker чтобы избежать проблем с зависимостями и сделать
|
||||||
|
окружение более повторяемым.
|
||||||
|
|
||||||
|
Клонируем [репозиторий с docker-compose](https://git.jt4d.ru/1vit/pgsql) и выполняем
|
||||||
|
`docker compose up`. Если команда не найдена, ставим compose
|
||||||
|
[пакетом](https://docs.docker.com/compose/install/linux/#install-using-the-repository) или
|
||||||
|
[из исходников](https://docs.docker.com/compose/install/linux/#install-the-plugin-manually).
|
||||||
|
|
||||||
|
Потом создаём в корне проекта (основного репозитория) файл `.env` (пример в `.env.example`). Он
|
||||||
|
используется для подключения к основной БД.
|
||||||
|
|
||||||
|
```
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_USERNAME=1vit_more
|
||||||
|
DB_PASSWORD=password
|
||||||
|
DB_DATABASE=1vit_more
|
||||||
|
```
|
||||||
|
|
||||||
|
Ещё рекомендуется создать `.env.test` (пример в `.env.test.example`), он будет использоваться для
|
||||||
|
тестов с использованием БД. Если файл не найден, будет перезаписываться основная база.
|
||||||
|
|
||||||
|
Проверяем подключение `yarn db:show` и накатываем миграции (создание структуры данных, schema)
|
||||||
|
`yarn db:migrate`.
|
||||||
|
|
||||||
|
Далее нужно заполнить БД демо-данными при помощи команды `yarn test-data:db-restore`.
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
| тип сервера | команда |
|
||||||
|
| ------------------------------------------------- | ------------------------------------------------------------------ |
|
||||||
|
| Полный dev сервер с БД | `yarn start` |
|
||||||
|
| Сторибук (для тестирования отдельных компонентов) | `yarn storybook` |
|
||||||
|
| Jest (unit-тесты) | `yarn test:unit` или `yarn test:unit path/to/file` |
|
||||||
|
| Jest (db-тесты) | `yarn test:db`, см. [доку про дб тесты](/docs/developing/database) |
|
||||||
|
|
||||||
|
## CI
|
||||||
|
|
||||||
|
При каждом `git push` запускается сборка приложения на
|
||||||
|
[Gitea Actions](https://git.jt4d.ru/1vit/more/actions).
|
||||||
|
|
||||||
|
Стенд для ветки доступен по адресу `http://<branch-name>.<STAGING_HOST>`.
|
||||||
|
|
||||||
|
Подробнее о системе деплоя, тегах и продакшн-деплое — в [разделе «Поставка»](/docs/deploy/ci-cd).
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# Ветки и пулл-реквесты
|
||||||
|
|
||||||
|
## Branches
|
||||||
|
|
||||||
|
Для работы над задачей создаётся ветка с названием формата `i<номер_issue>-<краткое_описание>`.
|
||||||
|
Краткое описание это БУКВАЛЬНО 1-2 слова. Примеры:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
i472-statements
|
||||||
|
i343-project-page
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pull requests
|
||||||
|
|
||||||
|
Сразу после загрузки (`git push`) ветки рекомендуется создавать ей pull request и привязать его к
|
||||||
|
задаче
|
||||||
|
|
||||||
|
Описание PR должно иметь вид список изменений - пустая строка - вид отношения к задаче и
|
||||||
|
\#номер_задачи, пример:
|
||||||
|
|
||||||
|
```md
|
||||||
|
- cover `viewRoot` flag in `users.dal` with db tests
|
||||||
|
- add `viewRole.findOne` test group
|
||||||
|
|
||||||
|
Closes #381
|
||||||
|
```
|
||||||
|
|
||||||
|
или
|
||||||
|
|
||||||
|
```md
|
||||||
|
- добавил тесты на наличие буквы, цифры, спецсимвола и на повторяемость.
|
||||||
|
- улучшен алгоритм генерации пароля
|
||||||
|
|
||||||
|
Closes #433
|
||||||
|
```
|
||||||
|
|
||||||
|
или
|
||||||
|
|
||||||
|
```md
|
||||||
|
- rename statements-period.types
|
||||||
|
- fix DateView stories
|
||||||
|
|
||||||
|
Related to #472
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Загружаем (`git push`) ветку
|
||||||
|
2. Создаём pull request с названием `WIP: <название_ветки>`
|
||||||
|
3. Добавляем описание
|
||||||
|
4. Добавляем PR в зависимости задачи (Dependencies справа)
|
||||||
|
|
||||||
|
Когда ветка готова, отправляем на ревью (Reviewers справа), убираем префикс `WIP` из названия и
|
||||||
|
перемещаем в столбец `To review` на [доске](http://git.arswarog.ru/1vit/more/projects/1).
|
||||||
|
|
||||||
|
## Доска проекта
|
||||||
|
|
||||||
|
Статус задач можно отслеживать на [доске проекта](http://git.arswarog.ru/1vit/more/projects/1). Pull
|
||||||
|
request на доску не добавляем, там должны быть только задачи. PR будет отображаться под задачей если
|
||||||
|
его добавить в зависимости.
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
# Файловая структура проекта
|
||||||
|
|
||||||
|
## Фронтенд
|
||||||
|
|
||||||
|
### Гостевой фронтенд
|
||||||
|
|
||||||
|
Располагается в `/src/visitor`
|
||||||
|
|
||||||
|
- Лендинг на главной странице
|
||||||
|
- Страница организатора
|
||||||
|
- Страница проекта
|
||||||
|
|
||||||
|
Все они имеют уникальный дизайн (на `scss`), и могут быть быстро изменены
|
||||||
|
|
||||||
|
#### Ограничения
|
||||||
|
|
||||||
|
Может обращаться только к `common`
|
||||||
|
|
||||||
|
### Клиентский фронтенд
|
||||||
|
|
||||||
|
Располагается в `/src/client`
|
||||||
|
|
||||||
|
- регистрация
|
||||||
|
- авторизация
|
||||||
|
- личный кабинет
|
||||||
|
- создание заявок
|
||||||
|
- панель эксперта
|
||||||
|
- панель модератора
|
||||||
|
- панель админа
|
||||||
|
|
||||||
|
#### Ограничения
|
||||||
|
|
||||||
|
Может обращаться только к `common`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Бэкенд
|
||||||
|
|
||||||
|
Располагается в папке `/src/server`
|
||||||
|
|
||||||
|
### Ограничения
|
||||||
|
|
||||||
|
Может обращаться только к `common` и `client`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Общие типы и функции
|
||||||
|
|
||||||
|
Располагаются в папке `/src/common`
|
||||||
|
|
||||||
|
### Ограничения
|
||||||
|
|
||||||
|
Не может обращаться ни к каким другим частям, т.к. является корнем
|
||||||
|
|
||||||
|
## Демо данные
|
||||||
|
|
||||||
|
Располагаются в папке `/src/common`
|
||||||
|
|
||||||
|
Требования:
|
||||||
|
|
||||||
|
- Файл с этими данными должен называться `<название>.demo.ts`, где `<название>` - это название
|
||||||
|
типа во множественном числе
|
||||||
|
- Переменная с демо данными должна начинаться с `demo` и иметь вид `demo<название>`
|
||||||
|
- Переменные с вариантами данных одного типа должны начинаться с `demo<название>` и иметь вид
|
||||||
|
`demo<название><название варианта>`
|
||||||
|
- Если демо данных одного типа больше одного значения, то эти значения располагаются в этом же
|
||||||
|
файле
|
||||||
|
|
||||||
|
Рекомендация:
|
||||||
|
|
||||||
|
В названии константы указывать её тип (`demoEvent: IEvent`).
|
||||||
|
|
||||||
|
### Пример
|
||||||
|
|
||||||
|
Пример организации демо данных участников:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
/// file: src/demoData/participants.ts
|
||||||
|
|
||||||
|
export const demoParticipantBase: IBaseParticipant = { ... }
|
||||||
|
|
||||||
|
export const demoParticipantKey: IKeyParticipant = { ... }
|
||||||
|
|
||||||
|
export const demoParticipants: IParticipant[] = [
|
||||||
|
demoParticipantBase,
|
||||||
|
demoParticipantKey,
|
||||||
|
]
|
||||||
|
```
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# Схема зависимостей модулей
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
|
||||||
|
Applications
|
||||||
|
Applications <|-- Evaluation
|
||||||
|
|
||||||
|
Auth
|
||||||
|
|
||||||
|
Client
|
||||||
|
|
||||||
|
Contests
|
||||||
|
Contests : ContestsService
|
||||||
|
Contests : ContestAreasService
|
||||||
|
Contests <|-- Requests
|
||||||
|
Contests <|-- Applications
|
||||||
|
Contests <|-- Evaluation
|
||||||
|
|
||||||
|
Events
|
||||||
|
Events : EventsService
|
||||||
|
Events <|-- Requests
|
||||||
|
|
||||||
|
note for Events "Events должен\n зависеть от Requests,\n а не наоборот"
|
||||||
|
|
||||||
|
Expenses
|
||||||
|
Expenses : ExpensesService
|
||||||
|
Expenses <|-- Statements
|
||||||
|
|
||||||
|
Export
|
||||||
|
Export : ExportService
|
||||||
|
Export <|-- Statements
|
||||||
|
Export <|-- Requests
|
||||||
|
|
||||||
|
Files
|
||||||
|
Files : FilesService
|
||||||
|
|
||||||
|
Organizators
|
||||||
|
Organizators : OrganizatorsService
|
||||||
|
Organizators <|-- Applications
|
||||||
|
Organizators <|-- Contests
|
||||||
|
Organizators <|-- Requests
|
||||||
|
|
||||||
|
Profile
|
||||||
|
|
||||||
|
Requests
|
||||||
|
Requests : RequestsService
|
||||||
|
Requests <|-- Expenses
|
||||||
|
Requests <|-- Statements
|
||||||
|
|
||||||
|
Statements
|
||||||
|
Statements : StatementsPeriodsService
|
||||||
|
|
||||||
|
Users
|
||||||
|
Users : UsersService
|
||||||
|
Users <|-- Client
|
||||||
|
Users <|-- Profile
|
||||||
|
```
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# Тестирование с использованием реального бэкенда
|
||||||
|
|
||||||
|
Поднимается полноценнный nest сервер, затем для каждого теста БД сбрасывается и восстанавливается из
|
||||||
|
снапшота (`/test-data/db/`)
|
||||||
|
|
||||||
|
### CI/CD
|
||||||
|
|
||||||
|
Для каждого прогона
|
||||||
|
|
||||||
|
- создаётся база тестовая данных
|
||||||
|
- запускаются тесты с бэком
|
||||||
|
- тестовая база удаляется
|
||||||
|
|
||||||
|
Для CI/CD отдельных настроек не требуется
|
||||||
|
|
||||||
|
### Настройка
|
||||||
|
|
||||||
|
Настройки для тестов хранятся в файле `.env.test`. Пример файла:
|
||||||
|
|
||||||
|
```env
|
||||||
|
DB_USERNAME=test_user
|
||||||
|
DB_PASSWORD=secret
|
||||||
|
DB_DATABASE=test_db
|
||||||
|
```
|
||||||
|
|
||||||
|
Все недостающие значения будут взяты из `.env`.
|
||||||
|
|
||||||
|
### Локальный запуск
|
||||||
|
|
||||||
|
При запуске тестов можно использовать команды
|
||||||
|
|
||||||
|
- `yarn test:db` запустит только тесты с БД
|
||||||
|
- `yarn test` запустит все тесты в проекте
|
||||||
|
|
||||||
|
### Написание тестов
|
||||||
|
|
||||||
|
Тесты должны иметь расширение `.db-spec.ts`.
|
||||||
|
|
||||||
|
Для создания и запуска бэка используется утилита `createTestingBackend()`. В возвращаемом ей
|
||||||
|
интерфейсе есть:
|
||||||
|
|
||||||
|
- метод `resetState()` - нужно запускать перед каждым тестом
|
||||||
|
- метод `destroy()` - вызывать в конце группы тестов
|
||||||
|
- поле `http` - результат вызова `INestApplication.getHttpServer()`.
|
||||||
|
|
||||||
|
Также есть утилита `authenticate`, которую можно использовать для входа.
|
||||||
|
|
||||||
|
Пример управления состоянием:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
let app: ITestingBackend;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
app = await createTestingBackend();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await app.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await app.resetState();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Пример теста:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
it('при создании AppForm имеет статус draft и не удалена', async () => {
|
||||||
|
const api = supertest(app.http);
|
||||||
|
|
||||||
|
const response = await api
|
||||||
|
.post('/v1/contests/1/forms')
|
||||||
|
.set('Cookie', await authenticate(api, 'admin'))
|
||||||
|
.send(exampleValidForm)
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
expect(response.body).toMatchObject({
|
||||||
|
draft: true,
|
||||||
|
deleted: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Тестирование изолированной части бэкенда
|
||||||
|
|
||||||
|
Для unit-тестов определённой части бэкенда можно использовать утилиту `createTestingHarness(app)`.
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const module = await Test.createTestingModule({
|
||||||
|
providers: [UsersDal, TimeService, createMockConfigProvider(authConfigToken)],
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forRoot({
|
||||||
|
...testOrmCredentials,
|
||||||
|
entities: [User, Contest, AuthToken],
|
||||||
|
synchronize: false,
|
||||||
|
logging: false,
|
||||||
|
}),
|
||||||
|
TypeOrmModule.forFeature([User]),
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
app = module.createNestApplication();
|
||||||
|
await app.init();
|
||||||
|
|
||||||
|
const harness = await createTestingHarness(app);
|
||||||
|
harness.resetState(); // имеет тот же интерфейс, что createTestingBackend
|
||||||
|
```
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
label: 'Разработчику'
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# Введение
|
||||||
|
|
||||||
|
## Зачем нужно логирование
|
||||||
|
|
||||||
|
### Цель
|
||||||
|
|
||||||
|
Логирование — это ключевой инструмент диагностики и анализа поведения системы. Оно фиксирует
|
||||||
|
**действия, ошибки и события**, позволяя:
|
||||||
|
|
||||||
|
- анализировать поведение приложения;
|
||||||
|
- находить причины ошибок и сбоев;
|
||||||
|
- отслеживать бизнес-процессы (например, запуск конкурса или назначение эксперта);
|
||||||
|
- проводить аудит действий пользователей;
|
||||||
|
- контролировать производительность и стабильность работы.
|
||||||
|
|
||||||
|
Хорошее логирование даёт **контекст и доказательства** — кто, когда и что сделал, с каким
|
||||||
|
результатом.
|
||||||
|
|
||||||
|
### Основные задачи логирования
|
||||||
|
|
||||||
|
Логирование решает как **технические**, так и **организационные** задачи.
|
||||||
|
|
||||||
|
**Технические:**
|
||||||
|
|
||||||
|
- **Диагностика:** восстановление хода событий при сбое.
|
||||||
|
- **Аналитика:** понимание того, как система используется.
|
||||||
|
- **Поддержка:** ускорение поиска причин ошибок в эксплуатации.
|
||||||
|
|
||||||
|
**Организационные:**
|
||||||
|
|
||||||
|
- **Аудит:** фиксация действий пользователей и администраторов.
|
||||||
|
- **Безопасность:** выявление попыток несанкционированного доступа.
|
||||||
|
|
||||||
|
## Уровни логирования
|
||||||
|
|
||||||
|
В системе используется библиотека **Pino**, интегрированная через `nestjs-pino`. Она поддерживает
|
||||||
|
стандартные уровни логирования, отражающие важность события.
|
||||||
|
|
||||||
|
| Уровень | Метод | Когда использовать | Пример |
|
||||||
|
| --------- | --------- | --------------------------------------------------- | -------------------------------- |
|
||||||
|
| **TRACE** | `trace()` | Максимальная детализация, пошаговая трассировка | Проверка цепочки вызовов |
|
||||||
|
| **DEBUG** | `debug()` | Отладочная информация о логике работы | Вывод промежуточных данных |
|
||||||
|
| **INFO** | `log()` | Обычные события нормальной работы | Создание проекта |
|
||||||
|
| **WARN** | `warn()` | Нежелательные, но некритичные ситуации | Отказ в доступе |
|
||||||
|
| **ERROR** | `error()` | Ошибки, требующие внимания и реакции | Исключение при сохранении |
|
||||||
|
| **FATAL** | `fatal()` | Критические сбои, приводящие к остановке приложения | Потеря соединения с базой данных |
|
||||||
|
|
||||||
|
> 💡 Все уровни логов фиксируются одинаково по структуре данных — различается только их
|
||||||
|
> **важность**.
|
||||||
|
|
||||||
|
## Различие между dev и prod
|
||||||
|
|
||||||
|
Логирование настроено так, чтобы быть **удобным в разработке** и **эффективным в продакшене**.
|
||||||
|
|
||||||
|
| Среда | Формат | Уровень по умолчанию | Особенности |
|
||||||
|
| --------------- | ----------------- | -------------------- | --------------------------------------------------------------------- |
|
||||||
|
| **Development** | человеко-читаемый | `debug` | Цвета, отступы, подробные данные — удобно для чтения в консоли. |
|
||||||
|
| **Production** | JSON | `info` | Машиночитаемый формат для централизованной агрегации и анализа логов. |
|
||||||
|
|
||||||
|
> Состав данных в логе всегда одинаков — меняется только способ отображения. Уровень логирования
|
||||||
|
> можно переопределить через переменные окружения.
|
||||||
|
|
||||||
|
Таким образом:
|
||||||
|
|
||||||
|
- в **разработке** акцент на удобство восприятия и диагностику;
|
||||||
|
- в **продакшене** — на структурированные данные и интеграцию с системами мониторинга (например,
|
||||||
|
Loki или ELK).
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# Подключение и использование
|
||||||
|
|
||||||
|
## Получение логгера
|
||||||
|
|
||||||
|
Логгер внедряется в любой сервис, контроллер или компонент через **dependency injection**. После
|
||||||
|
внедрения требуется указать **контекст**, чтобы логи было проще анализировать.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
@Injectable()
|
||||||
|
export class ProjectService {
|
||||||
|
constructor(private readonly logger: Logger) {
|
||||||
|
this.logger.setContext('Projects');
|
||||||
|
}
|
||||||
|
|
||||||
|
async createProject(data: CreateProjectDto) {
|
||||||
|
this.logger.log('Project created', { project: 42, user: data.ownerId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Контекст логгера
|
||||||
|
|
||||||
|
Контекст указывает, **к какому модулю или бизнес-процессу относится лог**. Он помогает группировать
|
||||||
|
события и быстрее понимать источник при анализе системы.
|
||||||
|
|
||||||
|
Контекст задаётся вручную при инициализации логгера:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
constructor(private readonly logger: Logger) {
|
||||||
|
this.logger.setContext('Evaluation');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Правила именования контекста
|
||||||
|
|
||||||
|
Хорошие контексты делают логи самодокументируемыми. Чтобы они оставались единообразными, следуйте
|
||||||
|
правилам:
|
||||||
|
|
||||||
|
1. **Основой контекста является название модуля**, отражающее область бизнес-логики. Примеры:
|
||||||
|
|
||||||
|
- `Evaluation` — процесс оценки заявок;
|
||||||
|
- `Contests` — управление конкурсами;
|
||||||
|
- `Users` — работа с пользователями.
|
||||||
|
|
||||||
|
2. **При необходимости уточнения** добавляется второй уровень через двоеточие:
|
||||||
|
|
||||||
|
- `Evaluation:ExpertAssignment` — назначение экспертов;
|
||||||
|
- `Applications:Moderation` — модерация проектов.
|
||||||
|
|
||||||
|
3. **Технические роли** (`Service`, `Controller` и т. п.) в названии **не используются**, так как
|
||||||
|
контекст должен отражать **бизнес-смысл**, а не структуру кода.
|
||||||
|
|
||||||
|
> 💬 Контекст — часть “языка логирования” проекта. Он должен быть **стабильным, понятным и
|
||||||
|
> единообразным** во всех модулях.
|
||||||
|
|
||||||
|
## Контекст запроса
|
||||||
|
|
||||||
|
При логировании внутри HTTP-запроса система автоматически добавляет поля:
|
||||||
|
|
||||||
|
- `user` — идентификатор пользователя (если авторизован);
|
||||||
|
- `url` — адрес текущего запроса.
|
||||||
|
|
||||||
|
Это позволяет связывать бизнес-события с конкретными действиями пользователей и упрощает анализ
|
||||||
|
логов.
|
||||||
|
|
||||||
|
## Переменные окружения
|
||||||
|
|
||||||
|
Настройки логирования задаются через `.env` или переменные окружения.
|
||||||
|
|
||||||
|
| Переменная | Назначение | Пример значения |
|
||||||
|
| ----------------- | -------------------------------------------------------- | -------------------------------- |
|
||||||
|
| `LOG_LEVEL` | Минимальный уровень логирования. | `debug`, `info`, `warn`, `error` |
|
||||||
|
| `LOG_PRETTY` | Включает человеко-читаемый формат (обычно в dev). | `true` |
|
||||||
|
| `LOG_SILENT_HTTP` | Отключает автоматические HTTP-логи (актуально в тестах). | `true` |
|
||||||
|
|
||||||
|
Если переменные не заданы, применяются значения по умолчанию:
|
||||||
|
|
||||||
|
| Окружение | Значения по умолчанию |
|
||||||
|
| --------- | ------------------------------------ |
|
||||||
|
| **dev** | `LOG_LEVEL=debug`, `LOG_PRETTY=true` |
|
||||||
|
| **prod** | `LOG_LEVEL=info`, `LOG_PRETTY=false` |
|
||||||
|
|
||||||
|
> 🔧 Уровень логирования можно переопределить без релиза — через переменные окружения при запуске
|
||||||
|
> приложения.
|
||||||
|
|
||||||
|
## Чек-лист перед ревью
|
||||||
|
|
||||||
|
- [ ] Логгер подключён через dependency injection.
|
||||||
|
- [ ] Контекст задан явно через `setContext()`.
|
||||||
|
- [ ] Уровень логирования соответствует окружению.
|
||||||
|
- [ ] Логи не содержат чувствительных данных.
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# Автоматическое логирование
|
||||||
|
|
||||||
|
Автоматическое логирование фиксирует системные события **без участия разработчика**. Оно
|
||||||
|
обеспечивает единый формат сообщений и полное покрытие ключевых операций платформы —
|
||||||
|
**HTTP-запросов**, **проверок доступа (RBAC)** и **ошибок**.
|
||||||
|
|
||||||
|
## Назначение
|
||||||
|
|
||||||
|
Цели автоматического логирования:
|
||||||
|
|
||||||
|
- Сократить количество ручных логов в коде.
|
||||||
|
- Обеспечить единообразие сообщений и уровней логирования.
|
||||||
|
- Повысить наблюдаемость и удобство диагностики.
|
||||||
|
- Позволить анализировать производительность и отказы без изменения бизнес-логики.
|
||||||
|
|
||||||
|
> 💡 Автоматические логи всегда присутствуют и формируются системой независимо от действий
|
||||||
|
> разработчика.
|
||||||
|
|
||||||
|
## Источники автоматических логов
|
||||||
|
|
||||||
|
| Тип события | Источник | Что логируется |
|
||||||
|
| --------------------------- | ---------------------------- | -------------------------------------------------------------- |
|
||||||
|
| **HTTP-запросы** | middleware `pino-http` | Каждый завершённый запрос с кодом ответа и временем обработки. |
|
||||||
|
| **Проверки доступа (RBAC)** | модуль `RbacModule` | Успешные и неуспешные проверки разрешений. |
|
||||||
|
| **Ошибки и исключения** | глобальный `ExceptionFilter` | Необработанные ошибки приложения и системные сбои. |
|
||||||
|
|
||||||
|
Все эти события регистрируются автоматически и не требуют дополнительного кода в модулях.
|
||||||
|
|
||||||
|
## HTTP-запросы
|
||||||
|
|
||||||
|
### Общие принципы
|
||||||
|
|
||||||
|
- Каждый HTTP-запрос логируется **после завершения обработки**.
|
||||||
|
- Лог формируется middleware `pino-http` на основе данных ответа.
|
||||||
|
- Разработчику не нужно добавлять логи вручную — они создаются автоматически.
|
||||||
|
|
||||||
|
Каждая запись содержит:
|
||||||
|
|
||||||
|
- HTTP-метод (`GET`, `POST`, `PATCH` и т. д.);
|
||||||
|
- путь запроса (`/api/v1/...`);
|
||||||
|
- код ответа (`200`, `403`, `500` и т. д.);
|
||||||
|
- время выполнения (в миллисекундах);
|
||||||
|
- идентификатор пользователя (`user`, если известен).
|
||||||
|
|
||||||
|
### Уровни логирования
|
||||||
|
|
||||||
|
| Категория ответа | Пример кода | Уровень |
|
||||||
|
| ---------------- | ----------- | ------- |
|
||||||
|
| Успешный ответ | `2xx` | `info` |
|
||||||
|
| Ошибка клиента | `4xx` | `info` |
|
||||||
|
| Ошибка сервера | `5xx` | `error` |
|
||||||
|
|
||||||
|
> Уровень `warn` для HTTP-логов не используется — это предотвращает появление ложных предупреждений
|
||||||
|
> при корректных отказах.
|
||||||
|
|
||||||
|
### Задачи HTTP-логов
|
||||||
|
|
||||||
|
- Отслеживание активности API.
|
||||||
|
- Измерение времени обработки запросов.
|
||||||
|
- Анализ причин ошибок и неудачных вызовов.
|
||||||
|
- Связь действий пользователей с конкретными маршрутами.
|
||||||
|
|
||||||
|
## Проверки доступа (RBAC)
|
||||||
|
|
||||||
|
### Общие принципы
|
||||||
|
|
||||||
|
Все проверки доступа логируются **автоматически** при вызовах методов `check*` и `accessDenied`. Это
|
||||||
|
обеспечивает аудит всех случаев — и разрешённых, и запрещённых действий.
|
||||||
|
|
||||||
|
### Уровни логирования
|
||||||
|
|
||||||
|
| Событие | Уровень | Что фиксируется |
|
||||||
|
| --------------- | ------- | -------------------------------------------------- |
|
||||||
|
| Отказ в доступе | `warn` | Попытка выполнить действие без нужного разрешения. |
|
||||||
|
| Доступ разрешён | `debug` | Успешная проверка разрешения. |
|
||||||
|
|
||||||
|
> Такое распределение уровней помогает разделять реальные проблемы (отказы) и штатные сценарии
|
||||||
|
> (успешные проверки).
|
||||||
|
|
||||||
|
## Ошибки и исключения
|
||||||
|
|
||||||
|
### Общие принципы
|
||||||
|
|
||||||
|
Глобальный `ExceptionFilter` автоматически логирует все необработанные ошибки:
|
||||||
|
|
||||||
|
- `error` — ошибки приложения (например, неверные данные или сбой бизнес-логики);
|
||||||
|
- `fatal` — критические сбои, приводящие к остановке приложения (например, невозможность
|
||||||
|
подключиться к базе данных).
|
||||||
|
|
||||||
|
### Структура записи
|
||||||
|
|
||||||
|
Каждая запись об ошибке включает:
|
||||||
|
|
||||||
|
- `trace` — стек вызовов;
|
||||||
|
- `context` — модуль, где произошла ошибка;
|
||||||
|
- `user` и `url` — если ошибка возникла в контексте HTTP-запроса.
|
||||||
|
|
||||||
|
> Это обеспечивает точную локализацию проблем и упрощает разбор инцидентов.
|
||||||
|
|
||||||
|
## Рекомендации
|
||||||
|
|
||||||
|
- Не дублируйте автоматические логи вручную.
|
||||||
|
- Если требуется зафиксировать **дополнительный контекст**, добавляйте отдельный лог рядом с
|
||||||
|
бизнес-событием — но не повторяйте уже зарегистрированные HTTP-запросы или проверки доступа.
|
||||||
|
- При анализе проблем **начинайте с автоматических логов** — они формируются всегда и имеют единый
|
||||||
|
формат.
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
# Ручное логирование и бизнес-события
|
||||||
|
|
||||||
|
Автоматическое логирование фиксирует системные события (HTTP-запросы, проверки доступа, ошибки), но
|
||||||
|
не отражает **внутреннюю бизнес-логику** приложения.
|
||||||
|
|
||||||
|
Ручное логирование используется для фиксации действий и изменений, важных **с точки зрения
|
||||||
|
продукта** — того, что видит или делает пользователь.
|
||||||
|
|
||||||
|
## Назначение
|
||||||
|
|
||||||
|
Ручные логи применяются для описания событий, которые отражают состояние системы и бизнес-процессы:
|
||||||
|
|
||||||
|
- создание, изменение и удаление сущностей;
|
||||||
|
- начало и завершение процессов;
|
||||||
|
- изменение статуса или состояния;
|
||||||
|
- ошибки бизнес-операций;
|
||||||
|
- любые другие действия, значимые для анализа и аудита.
|
||||||
|
|
||||||
|
> Такое логирование помогает понять, **что происходило внутри системы**, даже без включённой отладки
|
||||||
|
> или доступа к базе данных.
|
||||||
|
|
||||||
|
## Правила оформления логов
|
||||||
|
|
||||||
|
### Общие принципы
|
||||||
|
|
||||||
|
Сообщения должны быть:
|
||||||
|
|
||||||
|
- **краткими** — не длиннее одной фразы;
|
||||||
|
- **человеко-читаемыми** — понятными без знания кода;
|
||||||
|
- **однозначными** — описывать факт, а не процесс;
|
||||||
|
- **на английском**, **в прошедшем времени**, **без артиклей и пунктуации**.
|
||||||
|
|
||||||
|
Если операция завершилась неудачей, сообщение должно оканчиваться на `failed`.
|
||||||
|
|
||||||
|
Логирование выполняется **после завершения** операции — независимо от результата.
|
||||||
|
|
||||||
|
### Примеры сообщений
|
||||||
|
|
||||||
|
**Успешные операции:**
|
||||||
|
|
||||||
|
- `Project created`
|
||||||
|
- `Contest launched`
|
||||||
|
- `Review assigned`
|
||||||
|
- `Evaluation completed`
|
||||||
|
|
||||||
|
**Ошибки и неудачи:**
|
||||||
|
|
||||||
|
- `Project update failed`
|
||||||
|
- `File upload failed`
|
||||||
|
- `Review submission failed`
|
||||||
|
|
||||||
|
**Неправильные формулировки:**
|
||||||
|
|
||||||
|
- `Creating project...` — отражает процесс, а не факт.
|
||||||
|
- `Contest ${id} launched` — содержит шаблон и динамику.
|
||||||
|
- `Project created successfully!` — избыточно и разговорно.
|
||||||
|
|
||||||
|
### Дополнительные данные
|
||||||
|
|
||||||
|
Контекст события передаётся отдельным объектом (`details`). Включайте только данные, которые
|
||||||
|
действительно помогают при анализе.
|
||||||
|
|
||||||
|
**Рекомендации:**
|
||||||
|
|
||||||
|
- Всегда указывайте **идентификаторы** (`project`, `contest`, `user` и т. д.).
|
||||||
|
- В идентификаторах избегайте суфиксов `id` (пример: `project` вместо `projectId`).
|
||||||
|
- Добавляйте **причину** (`reason`) при ошибках или отказах.
|
||||||
|
- Указывайте **новое состояние** (`status`, `result`) при изменениях.
|
||||||
|
- Не включайте чувствительные данные — токены, e-mail, пароли, персональные сведения.
|
||||||
|
|
||||||
|
**Пример:**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
logger.log('Project created', { project: 42, user: 7 });
|
||||||
|
logger.error('Project update failed', { project: 42, reason: 'invalid_status' });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Бизнес-события
|
||||||
|
|
||||||
|
### Что считается бизнес-событием
|
||||||
|
|
||||||
|
Бизнес-события описывают **действия и изменения на уровне предметной области**, например:
|
||||||
|
|
||||||
|
- `Contest launched` — запуск конкурса;
|
||||||
|
- `Project submitted` — заявка подана;
|
||||||
|
- `Review assigned` — эксперт назначен;
|
||||||
|
- `Evaluation completed` — оценка завершена.
|
||||||
|
|
||||||
|
> Все бизнес-события логируются на уровне `info` (`logger.log()`), а ошибки бизнес-операций — на
|
||||||
|
> уровне `error`.
|
||||||
|
|
||||||
|
### Длительные процессы
|
||||||
|
|
||||||
|
Для долгих операций допускается логирование **двух фаз**:
|
||||||
|
|
||||||
|
- начало — `<Action> started`;
|
||||||
|
- завершение — `<Action>` (применяется обычные правила оформления сообщений).
|
||||||
|
|
||||||
|
**Пример:**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
logger.log('File export started', { contest: 3 });
|
||||||
|
logger.log('File exported', { contest: 3, durationMs: 14250 });
|
||||||
|
logger.error('File export failed', { contest: 3, reason: 'timeout' });
|
||||||
|
```
|
||||||
|
|
||||||
|
Такой подход позволяет отслеживать прогресс и измерять длительность операций.
|
||||||
|
|
||||||
|
### Зачем нужны бизнес-события
|
||||||
|
|
||||||
|
- Повышают прозрачность бизнес-процессов.
|
||||||
|
- Используются при анализе и аудите.
|
||||||
|
- Позволяют отследить жизненный цикл сущностей — от создания до завершения.
|
||||||
|
- Упрощают поиск причин ошибок на уровне сценариев пользователей.
|
||||||
|
|
||||||
|
## Чек-лист перед ревью
|
||||||
|
|
||||||
|
- [ ] Контекст логгера задан.
|
||||||
|
- [ ] Сообщение короткое, на английском и в прошедшем времени.
|
||||||
|
- [ ] В `details` указаны идентификаторы и причина (если применимо).
|
||||||
|
- [ ] Уровень логирования выбран корректно (`info` или `error`).
|
||||||
|
- [ ] Лог не дублирует автоматические записи (HTTP, RBAC).
|
||||||
|
- [ ] Отсутствуют конфиденциальные данные.
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
# Тестирование логирования
|
||||||
|
|
||||||
|
## Подключение
|
||||||
|
|
||||||
|
Для тестов следует подключать специальный модуль — **`LocalLoggerModule.forTesting()`**. Он
|
||||||
|
сохраняет логи в памяти и предоставляет к ним доступ через `TestingLogger`.
|
||||||
|
|
||||||
|
Это позволяет:
|
||||||
|
|
||||||
|
- перехватывать все вызовы логгера внутри тестируемого кода;
|
||||||
|
- сохранять их в память вместо вывода в консоль;
|
||||||
|
- проверять, какие именно логи были собраны за время теста.
|
||||||
|
|
||||||
|
Пример подключения через NestJS `TestingModule`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
let logger: TestingLogger;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module = await Test.createTestingModule({
|
||||||
|
imports: [LocalLoggerModule.forTesting()],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
logger = module.get(TestingLogger);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
После такого подключения любые вызовы вида:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
this.logger.log('Project created');
|
||||||
|
this.logger.error('Project update failed');
|
||||||
|
```
|
||||||
|
|
||||||
|
будут сохранены внутри `TestingLogger`, и вы сможете проверить их в тестах при помощи матчеров:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
logger.withMessage('Project created').withLevel('info').toBeLoggedOnce();
|
||||||
|
logger.withMessage('Project update failed').withLevel('error').toBeLogged();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Цепочка фильтров
|
||||||
|
|
||||||
|
Методы фильтрации (`withMessage`, `withLevel`, `withContext`, `withDetails`) можно вызывать
|
||||||
|
цепочкой, для более точного сопоставления логов:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
logger
|
||||||
|
.withMessage('Project created')
|
||||||
|
.withLevel('info')
|
||||||
|
.withContext('Projects')
|
||||||
|
.withDetails({ user: 5 })
|
||||||
|
.toBeLoggedOnce();
|
||||||
|
```
|
||||||
|
|
||||||
|
Метод `withDetails` поддерживает частичное сравнение объекта, а также может проверять поля `level`,
|
||||||
|
`context` и `message`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
logger
|
||||||
|
.withMessage('Project created')
|
||||||
|
.withDetails({
|
||||||
|
level: 'info',
|
||||||
|
context: 'Projects',
|
||||||
|
user: 5,
|
||||||
|
})
|
||||||
|
.toBeLoggedOnce();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Просмотр найденных записей
|
||||||
|
|
||||||
|
Если нужно отладить фильтр, и просмотреть, какие записи были найдены по текущему фильтру, можно
|
||||||
|
использовать метод `showLogs()`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
logger.withLevel('error').showLogs().toBeLogged();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Методы проверки
|
||||||
|
|
||||||
|
| Метод | Описание |
|
||||||
|
| -------------------- | --------------------------------------------------------- |
|
||||||
|
| `toBeLogged()` | Проверяет, что хотя бы одна запись соответствует фильтру. |
|
||||||
|
| `toBeNotLogged()` | Проверяет, что таких записей нет. |
|
||||||
|
| `toBeLoggedOnce()` | Проверяет, что запись встречается ровно один раз. |
|
||||||
|
| `toBeLoggedTimes(n)` | Проверяет, что запись встречается `n` раз. |
|
||||||
|
|
||||||
|
Все методы выбрасывают ошибку, если условие не выполняется.
|
||||||
|
|
||||||
|
> **Важно:** Методы проверки **обязательно** должны вызываться, иначе проверки так и не будет.
|
||||||
|
|
||||||
|
### Группирование проверок
|
||||||
|
|
||||||
|
Если в тесте нужно проверить несколько событий с общим набором полей, можно использовать такой
|
||||||
|
приём:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const rbacLogger = logger.withContext('RBAC');
|
||||||
|
|
||||||
|
rbacLogger.withMessage('Access granted').toBeLoggedTimes(2);
|
||||||
|
|
||||||
|
rbacLogger.withMessage('Access denied').toBeLoggedOnce();
|
||||||
|
```
|
||||||
|
|
||||||
|
Это удобно для группировки проверок по контексту или другим общим параметрам.
|
||||||
|
|
||||||
|
## Примеры использования
|
||||||
|
|
||||||
|
### Проверка бизнес-события
|
||||||
|
|
||||||
|
```ts
|
||||||
|
it('должен логировать создание проекта', () => {
|
||||||
|
service.createProject({ user: 7 });
|
||||||
|
|
||||||
|
logger
|
||||||
|
.withMessage('Project created')
|
||||||
|
.withLevel('info')
|
||||||
|
.withDetails({ user: 7 })
|
||||||
|
.toBeLoggedOnce();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проверка ошибок
|
||||||
|
|
||||||
|
```ts
|
||||||
|
it('должен логировать ошибку при невалидном статусе', () => {
|
||||||
|
service.updateProjectStatus(42, 'invalid');
|
||||||
|
|
||||||
|
logger.withMessage('Project update failed').withLevel('warn').toBeLogged();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проверка отсутствия логов
|
||||||
|
|
||||||
|
```ts
|
||||||
|
it('не должен логировать ничего при успешном выполнении без событий', () => {
|
||||||
|
service.doNothing();
|
||||||
|
|
||||||
|
logger.any().toBeNotLogged();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Очистка
|
||||||
|
|
||||||
|
```ts
|
||||||
|
beforeEach(() => {
|
||||||
|
logger.clear();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Используйте `clear()` для сброса состояния между тестами, если экземпляр `TestingLogger`
|
||||||
|
переиспользуется в нескольких сценариях.
|
||||||
|
|
||||||
|
## Рекомендации
|
||||||
|
|
||||||
|
- `TestingLogger` предназначен **только для тестов** — в рабочем коде используется обычный
|
||||||
|
`Logger`.
|
||||||
|
- Проверяйте **факт логирования**, а не реализацию — тесты должны оставаться устойчивыми к
|
||||||
|
внутренним изменениям.
|
||||||
|
- Используйте `toBeNotLogged()` для сценариев, где событие **не должно** быть зафиксировано.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
label: 'Логирование'
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
label: 'Технические модули'
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
label: 'Auth модуль и gateway'
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
# Принцип и сценарии
|
||||||
|
|
||||||
|
## Глоссарий
|
||||||
|
|
||||||
|
- **гость** не аутентифицированный посетитель
|
||||||
|
- **пользователь** аутентифицированный посетитель
|
||||||
|
|
||||||
|
## Назначение и требования к токенам
|
||||||
|
|
||||||
|
### Access token
|
||||||
|
|
||||||
|
- Короткоживущий многоразовый токен
|
||||||
|
- JWT
|
||||||
|
- За счет короткого времени жизни дополнительной проверки не требуется
|
||||||
|
|
||||||
|
### Refresh token
|
||||||
|
|
||||||
|
- Предназначен для одноразового получения нового комплекта токенов
|
||||||
|
- Токен должен храниться в базе данных и содержать следующую информацию:
|
||||||
|
- Разрешение на генерацию токена
|
||||||
|
- предыдущий refresh token
|
||||||
|
- аутентификация пользователя
|
||||||
|
- регистрация пользователя
|
||||||
|
- Т.к. токен одноразовый и периодически обновляется, то нельзя использовать sessionStorage для его
|
||||||
|
хранения
|
||||||
|
|
||||||
|
## Технические сценарии
|
||||||
|
|
||||||
|
### Гость на сайте
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant C as Client
|
||||||
|
participant G as Gateway
|
||||||
|
participant S as Server
|
||||||
|
|
||||||
|
C->>G: Request without access token
|
||||||
|
G->>S: Request without user info
|
||||||
|
S->>G: Response
|
||||||
|
G->>C: Response without tokens
|
||||||
|
```
|
||||||
|
|
||||||
|
### Гость логинится
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant C as Client
|
||||||
|
participant G as Gateway
|
||||||
|
participant S as Server
|
||||||
|
|
||||||
|
C->>S: Credentials
|
||||||
|
Note over S: Check credentials
|
||||||
|
alt valid and user active
|
||||||
|
S->>C: access_token, refresh_token
|
||||||
|
else invalid or user not active
|
||||||
|
S-->>C: 422 error
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Гость регистрируется
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant C as Client
|
||||||
|
participant G as Gateway
|
||||||
|
participant S as Server
|
||||||
|
|
||||||
|
C->>S: Credentials
|
||||||
|
Note over S: Check for no access token
|
||||||
|
Note over S: Check credentials
|
||||||
|
alt valid and user active
|
||||||
|
S->>C: Congratulation page with redirect on timeout to login
|
||||||
|
else invalid or user not active
|
||||||
|
S-->>C: 422 error
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Пользователь выполняет запрос с корректным токеном
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant C as Client
|
||||||
|
participant G as Gateway
|
||||||
|
participant S as Server
|
||||||
|
|
||||||
|
C->>G: Request with access token
|
||||||
|
Note over G: Check access token
|
||||||
|
G->>S: Request with session data
|
||||||
|
alt session data changed
|
||||||
|
S->>G: Change sesstion data
|
||||||
|
G->>C: Response with new access token
|
||||||
|
else no sesstion changed
|
||||||
|
S->>C: Response
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Пользователь выполняет запрос с некорректным токеном
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant C as Client
|
||||||
|
participant G as Gateway
|
||||||
|
participant S as Server
|
||||||
|
|
||||||
|
C->>G: Request with access and refresh tokens
|
||||||
|
Note over G: Check access token
|
||||||
|
Note over G: Access token invalid
|
||||||
|
Note over G: Check refresh token
|
||||||
|
alt refresh token valid, user is active
|
||||||
|
G->>S: Request with sesstion data
|
||||||
|
S->>G: Response with/without session data
|
||||||
|
Note over G: Create new tokens with session data
|
||||||
|
G->>C: Response with new access and refresh tokens
|
||||||
|
else refresh token expared
|
||||||
|
G-->>C: 401 Token expired
|
||||||
|
else refresh token already used
|
||||||
|
Note left of G: Leak warning
|
||||||
|
G-->>C: 419 Token already used, leak warning
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Если `refresh token` уже был ранее использован, то это может означать что токен ранее утек, потому
|
||||||
|
пользователю надо об этом сообщить, и, возможно заблокировать все refresh token'ы, выпущенные
|
||||||
|
благодаря потенциально утекшему
|
||||||
|
|
||||||
|
### Пользователь обновляет токены
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant C as Client
|
||||||
|
participant G as Gateway
|
||||||
|
participant S as Server
|
||||||
|
|
||||||
|
C->>G: Request with access and refresh tokens
|
||||||
|
Note over G: Check refresh token
|
||||||
|
alt refresh token valid, user is active
|
||||||
|
Note over G: Create new tokens with session data from access token
|
||||||
|
G->>C: Response with new access and refresh tokens
|
||||||
|
else refresh token expared
|
||||||
|
G-->>C: 401 Token expired
|
||||||
|
else refresh token already used
|
||||||
|
Note left of G: Leak warning
|
||||||
|
G-->>C: 419 Token already used, leak warning
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Если `refresh token` уже был ранее использован, то это может означать что токен ранее утек, потому
|
||||||
|
пользователю надо об этом сообщить, и, возможно заблокировать все refresh token'ы, выпущенные
|
||||||
|
благодаря потенциально утекшему
|
||||||
|
|
||||||
|
### Пользователь логинится
|
||||||
|
|
||||||
|
При переходе пользователя на страницу логина его перенаправляет на главную
|
||||||
|
|
||||||
|
### Пользователь регистрируется
|
||||||
|
|
||||||
|
Регистрация недоступна для аутентифицированного пользователя
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Настройки
|
||||||
|
|
||||||
|
## Порядок применения настроек
|
||||||
|
|
||||||
|
Настройки берутся из следующих мест (в порядке убывания приоритета):
|
||||||
|
|
||||||
|
### Переменные окружения
|
||||||
|
|
||||||
|
Ключ для каждой настройки формируется как SCREAMING_SNAKE_CASE из полного пути к настройке, включая
|
||||||
|
scope
|
||||||
|
|
||||||
|
### .env файл
|
||||||
|
|
||||||
|
Значения берутся из `.env` файла (для `NODE_ENV` = `test` из файла `.env.test`)
|
||||||
|
|
||||||
|
### Значения по умолчанию
|
||||||
|
|
||||||
|
Значения по умолчанию указаны в коде приложения
|
||||||
|
|
||||||
|
## Настройки для тестирования
|
||||||
|
|
||||||
|
Переменные окружения для теста можно указать, подменив значение провайдера `VIRTUAL_ENV`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
imports: [ConfigModule],
|
||||||
|
})
|
||||||
|
.overrideProvider(VIRTUAL_ENV)
|
||||||
|
.useValue({ NEW_OPTION: 'someValue' })
|
||||||
|
.compile();
|
||||||
|
```
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# Общие концепции
|
||||||
|
|
||||||
|
## Что такое DataStoreModule
|
||||||
|
|
||||||
|
`DataStoreModule` — это сервисный модуль в NestJS, предназначенный для хранения **внутренних данных
|
||||||
|
feature-модулей**. Он работает через концепцию **bucket** (аналог внутреннего S3 или localStorage
|
||||||
|
для конкретного модуля).
|
||||||
|
|
||||||
|
> Важно: `DataStoreModule` не имеет отношения к S3 и используется только для хранения служебных
|
||||||
|
> данных конкретного модуля.
|
||||||
|
|
||||||
|
Каждый модуль получает собственное пространство хранения, изолированное от других. Даже если два
|
||||||
|
разных модуля используют bucket с одинаковым именем, они всё равно будут независимы.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Основные концепции
|
||||||
|
|
||||||
|
### Bucket
|
||||||
|
|
||||||
|
- Логическое пространство хранения внутри модуля.
|
||||||
|
- Для каждого конкурса используется отдельный bucket.
|
||||||
|
- Если требуется хранить данные, не связанные с конкурсом, используется `contestId = 0`.
|
||||||
|
- Имеет имя (например, `"test"`).
|
||||||
|
- Может использоваться для разных типов данных: настройки, результаты модерации и т.д.
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
|
||||||
|
- `bucket: settings` → хранение общих настроек модуля.
|
||||||
|
- `bucket: moderationResults` → хранение результатов модерации.
|
||||||
|
|
||||||
|
### Feature-модуль
|
||||||
|
|
||||||
|
Каждый бизнес-модуль (например, `FooModule`, `BarModule`) подключает `DataStoreModule` через метод
|
||||||
|
`forFeature`. В результате внутри модуля можно работать с одним или несколькими bucket’ами.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Поддерживаемые сценарии
|
||||||
|
|
||||||
|
- **Работа в production** — `forRoot` подключает модуль к базе данных, а `forFeature` регистрирует
|
||||||
|
bucket’ы для конкретного feature.
|
||||||
|
- **Тестирование** — `forTesting` создаёт in-memory версию, совместимую по API с production, но
|
||||||
|
без использования БД.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Зачем использовать DataStoreModule
|
||||||
|
|
||||||
|
- Унифицированное API для хранения служебных данных модулей без необходимости создания собственных
|
||||||
|
таблиц.
|
||||||
|
- Автоматическое разделение данных между модулями и конкурсами.
|
||||||
|
- Простая интеграция в любой feature-модуль.
|
||||||
|
|
||||||
|
### Когда использовать DataStoreModule
|
||||||
|
|
||||||
|
- Временное хранение данных, не требующих сложных запросов.
|
||||||
|
- Хранение данных, не требующих частого обращения и сложных запросов.
|
||||||
|
- Хранение данных при прототипировании.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# Использование
|
||||||
|
|
||||||
|
## Подключение в AppModule
|
||||||
|
|
||||||
|
В корневом модуле приложения необходимо инициализировать `DataStoreModule` с помощью метода
|
||||||
|
`forRoot`. В production используется хранилище на базе БД.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// app.module.ts
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { DataStoreModule } from '@src/server/data-store';
|
||||||
|
import { FooModule } from './foo/foo.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [DataStoreModule.forRoot(), FooModule],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Подключение bucket в feature-модуле
|
||||||
|
|
||||||
|
Каждый feature-модуль определяет, какие bucket’ы ему нужны, через метод forFeature.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// foo.module.ts
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { DataStoreModule } from '@src/server/data-store';
|
||||||
|
import { FooService } from './foo.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [DataStoreModule.forFeature('foo', ['settings'])],
|
||||||
|
providers: [FooService],
|
||||||
|
exports: [FooService],
|
||||||
|
})
|
||||||
|
export class FooModule {}
|
||||||
|
```
|
||||||
|
|
||||||
|
В примере выше модуль `foo` получает доступ к bucket с именем `settings`.
|
||||||
|
|
||||||
|
> Важно: В feature-модуле будут доступны только bucket'ы указанные в `forFeature`. Попытка
|
||||||
|
> инжектировать не объявленный bucket приведёт к ошибке.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Инжектирование bucket в сервис
|
||||||
|
|
||||||
|
Для работы с bucket используется декоратор `@InjectDataStore(bucketName)`. Каждый сервис получает
|
||||||
|
свой экземпляр DataStore для конкретного bucket.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// foo.service.ts
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectDataStore, DataStore } from '@src/server/data-store';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class FooService {
|
||||||
|
constructor(
|
||||||
|
@InjectDataStore('settings')
|
||||||
|
private readonly bucket: DataStore<string, { value: string }>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async saveValue(key: string, value: string) {
|
||||||
|
await this.bucket.set(1, key, { value });
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadValue(key: string) {
|
||||||
|
return this.bucket.get(1, key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Независимость bucket’ов разных модулей и конкурсов
|
||||||
|
|
||||||
|
Даже если несколько модулей используют bucket с одинаковым именем (`'test'`), они будут полностью
|
||||||
|
изолированы.
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
|
||||||
|
- FooModule с bucket `test` и
|
||||||
|
- BarModule с bucket `test`
|
||||||
|
|
||||||
|
получат разные хранилища, и данные не будут пересекаться.
|
||||||
|
|
||||||
|
Тоже самое и для разных конкурсов: данные для `contestId = 1` и `contestId = 2` будут храниться
|
||||||
|
отдельно.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Резюме
|
||||||
|
|
||||||
|
- В `AppModule` подключаем `DataStoreModule.forRoot(...)`.
|
||||||
|
- В каждом feature-модуле объявляем нужные bucket’ы через `forFeature(...)`.
|
||||||
|
- Доступ к bucket в сервисе осуществляется через `@InjectDataStore`.
|
||||||
|
- Bucket’ы одного имени в разных модулях изолированы друг от друга.
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Тестирование
|
||||||
|
|
||||||
|
## Зачем нужен тестовый режим
|
||||||
|
|
||||||
|
Для юнит- и интеграционных тестов используется метод `forTesting`. В этом случае `DataStoreModule`
|
||||||
|
работает на **in-memory движке**:
|
||||||
|
|
||||||
|
- API полностью совпадает с production-режимом,
|
||||||
|
- данные не сохраняются между перезапусками,
|
||||||
|
- не требуется подключение к реальной базе данных.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Подключение в тестах
|
||||||
|
|
||||||
|
Вместо `forRoot` в тестовом модуле подключается `forTesting`.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// example.spec.ts
|
||||||
|
import { Test } from '@nestjs/testing';
|
||||||
|
import { DataStoreModule } from '@src/server/data-store';
|
||||||
|
import { FooModule } from './foo/foo.module';
|
||||||
|
|
||||||
|
describe('FooModule', () => {
|
||||||
|
it('Инициализация', async () => {
|
||||||
|
const module = await Test.createTestingModule({
|
||||||
|
imports: [DataStoreModule.forTesting(), FooModule],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
expect(() => module.createNestApplication()).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Доступ к данным
|
||||||
|
|
||||||
|
Для задания данных в тестах достаточно получить бакет через сервис модуля:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
it('Запись данных в bucket', async () => {
|
||||||
|
const module = await Test.createTestingModule({
|
||||||
|
imports: [DataStoreModule.forTesting(), FooModule, BarModule],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
const dataStore = module.get(DataStoreService);
|
||||||
|
const barService = module.get(BarService);
|
||||||
|
|
||||||
|
const bucket = dataStore.bucket('bar', 'test'); // доступ к bucket "test" модуля "bar"
|
||||||
|
await bucket.set(1, 'key', { value: 'bar' });
|
||||||
|
|
||||||
|
expect(await barService.bucket.get(1, 'key')).toEqual({ value: 'bar' });
|
||||||
|
});
|
||||||
|
```
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
label: 'DataStore'
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# Документы и изображения
|
||||||
|
|
||||||
|
## Общее устройство
|
||||||
|
|
||||||
|
Все загружаемые файлы хранятся в S3 хранилище, на сервер приложения файлы могут сохраняться только
|
||||||
|
на время обработки и только в директорию для временных файлов.
|
||||||
|
|
||||||
|
Изображение так же является документом, только с дополнительными возможностями
|
||||||
|
|
||||||
|
Индентификатор документа - это непорядковый длинный код
|
||||||
|
|
||||||
|
Отдельной системы контроля доступа нет, если пользователь знает ID файла - он может к нему
|
||||||
|
обратиться
|
||||||
|
|
||||||
|
Обновление файлов не предусмотрено, если нужно обновить - то загружается новый файл, и в нужном
|
||||||
|
месте указывается уже новый идентификатор, обновлять можно только название и описание.
|
||||||
|
|
||||||
|
## Документ
|
||||||
|
|
||||||
|
### Сохраняемая информация
|
||||||
|
|
||||||
|
В базе данных о каждом документе хранится информация.
|
||||||
|
|
||||||
|
- оригинальное имя файла (используется при скачивании)
|
||||||
|
- время загрузки файла
|
||||||
|
- пользователь, загрузивший файл
|
||||||
|
- mime тип файл
|
||||||
|
- общий тип файла, пока только документ или изображение
|
||||||
|
- так же файлу можно задать название документа и описание
|
||||||
|
- размер файла
|
||||||
|
|
||||||
|
### Доступные действия
|
||||||
|
|
||||||
|
- получение информации о файле (`GET /files/:id`)
|
||||||
|
- получение информации о нескольких файлах (`GET /files/many/:id,:id,:id`)
|
||||||
|
- просмотреть (`GET /files/:id/view`)
|
||||||
|
- скачать (`GET /files/:id/download`) отличается от **посмотреть** тем, что указываются заголовки
|
||||||
|
для скачивания
|
||||||
|
- загрузка файла (`POST /files`), возвращает структуру `IDocument` или дочернюю
|
||||||
|
|
||||||
|
## Изображение
|
||||||
|
|
||||||
|
Изображение так же являются документом, и для него доступны те же действия и возможности
|
||||||
|
|
||||||
|
### Сохраняемая информация
|
||||||
|
|
||||||
|
Дополнительно сохранятся размер изображения
|
||||||
|
|
||||||
|
### Дополнительные доступные действия
|
||||||
|
|
||||||
|
- просмотреть с определенными размерами (`GET /files/:id/view/:size`)
|
||||||
|
|
||||||
|
где `size` это Enum с заранее определенными размерами и параметрами
|
||||||
|
|
||||||
|
Для просмотра изображения используется сервер, на лету меняющий размеры изображения, для этого
|
||||||
|
сервер приложения выполняет редирект на специально подготовленный адрес
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
label: 'UI/UX'
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# Вкладки
|
||||||
|
|
||||||
|
Допустим нам нужно разделить страницу `users/page.tsx` на 2 вкладки:
|
||||||
|
|
||||||
|
- Основные данные
|
||||||
|
- Организации
|
||||||
|
|
||||||
|
При открытии страницы по умолчанию должна открываться вкладка "Основные данные".
|
||||||
|
|
||||||
|
Организуем структуру следующим образом:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
users / page.tsx; // корневая страница
|
||||||
|
general / page.tsx; // страница вкладки
|
||||||
|
organizators / page.tsx; // страница вкладки
|
||||||
|
```
|
||||||
|
|
||||||
|
## Определение вкладок
|
||||||
|
|
||||||
|
Добавим файл с определением вкладок:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// users/tabs.ts
|
||||||
|
|
||||||
|
import { ITabs } from '@src/client/uikit/TabBar';
|
||||||
|
|
||||||
|
export enum UserTab {
|
||||||
|
General = 'general',
|
||||||
|
Organizators = 'organizators',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const clientUserTabs: ITabs<UserTab> = [
|
||||||
|
{
|
||||||
|
label: 'Основные данные',
|
||||||
|
slug: UserTab.General,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Организации',
|
||||||
|
slug: UserTab.Organizators,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
И сделать по странице для каждой вкладки, _имя страницы_ должно совпадать с _текстовым значением_
|
||||||
|
варианта enum.
|
||||||
|
|
||||||
|
## Страницы вкладок
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// users/general/page.tsx
|
||||||
|
|
||||||
|
import { clientUserTabs } from '../tabs';
|
||||||
|
|
||||||
|
export default async function UserViewPage({ params }: { id: string }) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<Card.Header />
|
||||||
|
<TabBar items={clientUserTabs} />
|
||||||
|
<Card.Body />
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Компонент `TabBar` предназначен для использования непосредственно внутри `Card`. Если поместить его
|
||||||
|
в `Card.Header`, стили могут сломаться.
|
||||||
|
|
||||||
|
## Корневая страница
|
||||||
|
|
||||||
|
Корневые страницы (`users/page.tsx` в нашем примере) не поддерживаются, поскольку имя страницы
|
||||||
|
привязано к варианту enum, а корневая страница была бы пустой строкой.
|
||||||
|
|
||||||
|
Если она вам нужна, создайте страницу для редиректа:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// users/page.tsx
|
||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
interface IProps {
|
||||||
|
params: { id: string };
|
||||||
|
}
|
||||||
|
export default function UserViewPage({ params }: IProps) {
|
||||||
|
redirect(`/admin/users/${params.id}/general`);
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# CI/CD и деплой
|
||||||
|
|
||||||
|
Система CI построена на **Gitea Actions** (`.gitea/workflows/`). Два основных сценария: ветка и тег.
|
||||||
|
|
||||||
|
## Ветки → стенды
|
||||||
|
|
||||||
|
При каждом `git push` в любую ветку (включая `master`) автоматически:
|
||||||
|
|
||||||
|
1. Собирается проект (`yarn build`)
|
||||||
|
2. Публикуются Docker-образы в registry `git.jt4d.ru/1vit/more/`:
|
||||||
|
- `web:<branch>`
|
||||||
|
- `api:<branch>`
|
||||||
|
- `ingress:<branch>`
|
||||||
|
3. Разворачивается стенд на стейджинге по адресу `http://<branch>.<STAGING_HOST>`
|
||||||
|
|
||||||
|
Каждый стенд получает изолированную базу данных. Миграции запускаются автоматически при деплое.
|
||||||
|
|
||||||
|
При удалении ветки стенд и образы удаляются автоматически.
|
||||||
|
|
||||||
|
## Теги → релизные образы
|
||||||
|
|
||||||
|
При `git push` тега (например `v1.18.0`) запускается workflow `release.yaml`:
|
||||||
|
|
||||||
|
1. Собирается проект
|
||||||
|
2. Публикуются Docker-образы с **двумя тегами** — именем тега и `latest`:
|
||||||
|
- `git.jt4d.ru/1vit/more/web:v1.18.0`
|
||||||
|
- `git.jt4d.ru/1vit/more/web:latest`
|
||||||
|
- Аналогично для `api` и `ingress`
|
||||||
|
|
||||||
|
Стенд при push тега **не создаётся**.
|
||||||
|
|
||||||
|
Чтобы выпустить релиз:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag v1.18.0
|
||||||
|
git push origin v1.18.0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Продакшн-деплой
|
||||||
|
|
||||||
|
Для деплоя на продакшн используется `docker-compose.production.yml`. Образы берутся из Gitea
|
||||||
|
registry.
|
||||||
|
|
||||||
|
Развернуть последнюю версию (`latest`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PROD_HOST=example.com \
|
||||||
|
DB_HOST=db DB_USERNAME=user DB_PASSWORD=pass DB_DATABASE=mydb \
|
||||||
|
docker compose -f docker-compose.production.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Развернуть конкретную версию:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TAG=v1.18.0 PROD_HOST=example.com \
|
||||||
|
DB_HOST=db DB_USERNAME=user DB_PASSWORD=pass DB_DATABASE=mydb \
|
||||||
|
docker compose -f docker-compose.production.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
После деплоя не забыть прогнать миграции:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec 1vit_more_prod_api node_modules/.bin/typeorm migration:run -d data-source.js
|
||||||
|
```
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
label: 'Поставка'
|
||||||
Reference in New Issue
Block a user