diff --git a/students/Tsuytskou_Kiryl/lab_01/README.md b/students/Tsuytskou_Kiryl/lab_01/README.md new file mode 100644 index 00000000..d8dc7bc5 --- /dev/null +++ b/students/Tsuytskou_Kiryl/lab_01/README.md @@ -0,0 +1,335 @@ +

Министерство образования Республики Беларусь

+

Учреждение образования

+

"Брестский Государственный технический университет"

+

Кафедра ИИТ

+





+

Лабораторная работа №1

+

По дисциплине: "Проектирование интернет-систем"

+

Тема: "Сценарий транзакции: моделирование use-case и границ ответственности"

+





+

Выполнил:

+

Студент 3 курса

+

Группы ПО-13

+

Тютьков К.О.

+

Проверил:

+

Несюк А.Н.

+




+

Брест 2026

+ +--- + +## Цель работы + +Научиться анализировать бизнес-процессы интернет-системы, выявлять границы ответственности компонентов и моделировать транзакционные сценарии с учётом возможных сбоев. + +--- + +## Вариант №8 - Доска объявлений «Бери, пока горячее» + +**Питч:** _От велосипеда до учебника - всё тут._ + +**Ядро домена:** _Объявления, Категории, Чаты, Модерация_ + + +--- + +## Ход выполнения работы + +### 1. Структура проекта + +``` +lab-01/ +├── README.md # Основной отчёт (этот документ) +├── use-case.md # Текстовое описание use-case +├── diagrams/ +│ ├── sequence-happy.puml # PlantUML для успешного сценария +│ ├── sequence-happy.png # Экспорт диаграммы +│ ├── sequence-error-payment.puml +│ └── sequence-error-payment.png +├── scenarios.feature # Gherkin-сценарии +└── analysis.md # Анализ границ ответственности +``` + +--- + +### 2. Use-case описание + +👉 **Ссылка на файл:** [use-case.md](use-case.md) + +**Основной сценарий:** _Публикация объявления_ + +**Первичный актор:** _Продавец (Пользователь)_ + +**Цель:** _Быстро разместить объявление о продаже товара так, чтобы оно прошло базовую проверку и стало доступно другим пользователям._ + +**Краткое описание основного потока:** +1. Пользователь нажимает кнопку «Подать объявление». +2. Система отображает форму с полями: Категория, Заголовок, Описание, Цена, Фото (до 5 шт.). +3. Пользователь заполняет форму и нажимает «Опубликовать». +4. Система проверяет обязательные поля (заголовок, цена, категория, фото). +5. Система отправляет фото в Image Store и получает URL'ы. +6. Система запускает пре-модерацию (базовый спам-фильтр по тексту). +7. Система не находит запрещённых слов и признаков мошенничества. +8. Система сохраняет объявление в БД со статусом «Активно». +9. Система отправляет асинхронное уведомление (Push/Email): «Ваше объявление опубликовано». +10. Система возвращает пользователю ссылку на просмотр объявления. +11. Пользователь видит статус «Активно. Ждём покупателей!». + +**Альтернативные потоки:** +- Сохранение объявления как черновик без публикации +- Платное поднятие объявления в топ (буст) +- Создание объявления с минимальным бюджетом + +**Исключительные ситуации:** +- ML-модератор недоступен (таймаут) +- Хранилище изображений недоступно (Image Store) +- База данных недоступна +- Дубликат объявления от того же продавца + +--- + +### 3. Диаграммы последовательности (Sequence Diagrams) + +#### 3.1. Happy Path (успешный сценарий) + +👉 **PlantUML исходник:** [sequence-happy.puml](diagrams/sequence-happy.puml) + +![Диаграмма успешного сценария](diagrams/sequence-happy.png) + +**Описание потока:** +1. Продавец заполняет форму и загружает фото +2. Система загружает фото в Image Store +3. Система проверяет текст через ML-модератора (вердикт CLEAN) +4. Система сохраняет объявление в БД со статусом ACTIVE +5. Асинхронно отправляется email продавцу +6. Асинхронно происходит индексация в поиске + +**Участники:** +- Продавец (актор) +- Web UI (фронтенд) +- API Gateway (шлюз) +- Ad Service (сервис объявлений) +- Image Store (S3/MinIO) +- ML Moderator (анти-спам API) +- PostgreSQL (база данных) +- Event Bus (RabbitMQ) +- Search Worker (индексация в Elasticsearch) +- Notification Worker (email/push уведомления) + +#### 3.2. Error Case (сценарий с ошибкой) + +👉 **PlantUML исходник:** [sequence-error-payment.puml](diagrams/sequence-error-payment.puml) + +![Диаграмма сценария с ошибкой](diagrams/sequence-error-payment.png) + +**Описание потока:** +- Система успешно создаёт объявление и сохраняет фото +- При попытке проверить текст ML-модератор не отвечает (таймаут) +- Система не откатывает создание объявления +- Объявление сохраняется со статусом "PENDING_MODERATION" +- Событие о неудачной модерации публикуется в очередь +- Фоновый worker через 30 секунд повторяет запрос к ML +- При успешной повторной попытке статус меняется на ACTIVE и отправляется email продавцу + +--- + +### 4. Gherkin-сценарии + +👉 **Ссылка на файл:** [scenarios.feature](scenarios.feature) + +**Реализовано сценариев:** _5_ + +**Список сценариев:** +1. ✅ **Успешный сценарий:** Публикация объявления +2. ✅ **Ошибка:** Цена объявления отрицательная (валидация) +3. ✅ **Ошибка:** ML-модератор недоступен (таймаут с retry) +4. ✅ **Ошибка:** Хранилище изображений недоступно (откат транзакции) +5. ✅ **Ошибка:** Дубликат объявления от того же продавца + +**Пример сценария:** +```gherkin +Feature: Публикация объявления на доске «Бери, пока горячее» + Как Продавец + Я хочу быстро размещать объявления о продаже товаров + Чтобы находить покупателей и продавать вещи + + Scenario: Успешная публикация объявления + Given продавец авторизован как "seller@example.com" + And в системе есть категория "Электроника" + When продавец создаёт объявление с заголовком "iPhone 13" + And ценой "45000" и описанием "В идеальном состоянии" + And загружает 3 фотографии + And нажимает "Опубликовать" + Then система загружает фото в хранилище + And ML-модератор возвращает вердикт "CLEAN" + And система создаёт объявление со статусом "ACTIVE" + And система отправляет email продавцу + And продавец видит сообщение об успешной публикации +``` + +--- + +### 5. Анализ границ ответственности + +👉 **Ссылка на файл:** [analysis.md](analysis.md) + +#### 5.1. Транзакционные границы + + Операция | Синхронная/Асинхронная | Откат при ошибке | Retry-стратегия | Идемпотентность | +|----------|------------------------|------------------|-----------------|-----------------| +| **Валидация входных данных (цена, заголовок)** | Синхронная | Нет (просто возврат ошибки) | N/A | Да | +| **Загрузка фото в Image Store** | Синхронная | Да (ROLLBACK транзакции) | 3 попытки (1с, 2с, 4с) | Да (по хешу файла) | +| **Проверка ML-модератором** | Синхронная | Нет (перевод в PENDING_MODERATION) | 5 попыток (30с,1м,2м,5м,15м), затем ручная | Да (по ad_id) | +| **Создание записи Ad в БД** | Синхронная | Да (ROLLBACK транзакции) | Нет | Да (по idempotency_key) | +| **Генерация уникального ad_id** | Синхронная | Нет | N/A | Да (UUID v4) | +| **Публикация доменных событий (AdCreated)** | Синхронная (outbox) | Да | Нет | Да (по event_id) | +| **Отправка email продавцу** | Асинхронная | Нет | 5 попыток (1м,5м,15м,1ч,6ч) | Да (по ad_id + email) | +| **Индексация в Elasticsearch** | Асинхронная | Нет | 3 попытки | Да (ad_id как _id) | +| **Запись в журнал событий** | Асинхронная | Нет (best-effort) | 3 попытки | Да (по event_id) | + +#### 5.2. Обработка исключительных ситуаций + +**Реализовано стратегий обработки:** _5_ + +**Примеры:** + +##### Исключительная ситуация 1: _Таймаут ML-модератора (Anti-spam API недоступен)_ + +- **Условие возникновения:** ML-модератор не отвечает в течение 3 секунд или возвращает HTTP 503 Service Unavailable +- **Обнаружение:** HTTP-клиент выбрасывает TimeoutException или получает статус-код 5xx. Система логирует: "ML moderator timeout for ad_id=A-2026-0142" +- **Реакция:** + 1. Система НЕ откатывает создание объявления (фото уже загружены в Image Store) + 2. Объявление сохраняется в БД со статусом "PENDING_MODERATION" + 3. Система публикует событие "AdPendingModeration" в очередь `moderation_queue` с retry_count=0 + 4. Фоновый worker запускает повторную проверку по расписанию +- **Компенсация:** + - Worker пытается проверить текст повторно: 30с, 1м, 2м, 5м, 15м (exponential backoff) + - После 5 неудачных попыток объявление отправляется живому модератору + - Отправляется уведомление администратору: "ML-модератор недоступен, накоплено X задач" + - При успешной проверке на любом этапе статус меняется на "ACTIVE" +- **Уведомление пользователя:** "Объявление отправлено на проверку. Обычно это занимает 5 минут. Вы получите уведомление на email." + +##### Исключительная ситуация 2: _Хранилище изображений недоступно (Image Store timeout)_ + +- **Условие возникновения:** S3/MinIO не отвечает в течение 5 секунд при загрузке фото или возвращает HTTP 503 +- **Обнаружение:** HTTP-клиент выбрасывает TimeoutException. Система логирует: "Image Store timeout for ad_id=A-2026-0142, upload attempt 1/3" +- **Реакция:** + 1. Система предпринимает 3 попытки загрузки с интервалом 1с, 2с, 4с + 2. Если все 3 попытки не удались → транзакция ПОЛНОСТЬЮ откатывается (ROLLBACK) + 3. Объявление НЕ создаётся в БД + 4. Частично загруженные фото удаляются из хранилища (компенсация) +- **Компенсация:** + - Фронтенд сохраняет фото в IndexedDB/LocalStorage + - При восстановлении связи пользователю предлагается синхронизировать данные + - Фоновый процесс на клиенте повторяет загрузку +- **Уведомление пользователя:** "Не удалось загрузить фото. Попробуйте позже или выберите другие изображения." + +--- + +## Таблица критериев оценки + +| Критерий | Баллы | Выполнено | +|----------|-------|-----------| +| Use-case описание (полнота: акторы, предусловия, основной поток, альтернативы, исключения) | 15 | ✅ | +| Sequence diagram (happy path) - корректность нотации UML, включение всех ключевых компонентов | 20 | ✅ | +| Sequence diagram (error case) - моделирование хотя бы одной исключительной ситуации | 15 | ✅ | +| Gherkin-сценарии - минимум 4 сценария (1 успешный + 3 ошибочных) | 20 | ✅ | +| Анализ границ ответственности - таблица транзакционных границ, обоснование выбора синхронных/асинхронных операций | 15 | ✅ | +| Обработка исключений - описание стратегий retry, компенсации, уведомлений | 10 | ✅ | +| Качество документации - оформление, читаемость, грамотность | 5 | ✅ | +| **ИТОГО** | **100** | ✅ | + +--- + +## Контрольные вопросы + +**Подготовка к защите:** + +1. **Что такое транзакционная граница? Где она проходит в вашем сценарии?** + + Транзакционная граница определяет набор операций, которые должны выполняться атомарно (всё или ничего). В моём сценарии выделена одна транзакционная граница: + + - **Транзакция №1 (создание объявления):** Начинается при нажатии кнопки «Опубликовать», заканчивается записью объявления в БД со статусом "ACTIVE" (или "PENDING_MODERATION"). Включает валидацию данных, загрузку фото в Image Store, проверку ML-модератором, создание записи Ad. + +2. **Почему операция X выбрана синхронной, а Y - асинхронной?** + + **Синхронные операции (создание объявления, загрузка фото, ML-проверка):** + - Критичны для бизнес-процесса + - Без них объявление не может считаться созданным + - Требуют немедленного подтверждения пользователю + - Влияют на целостность данных + + **Асинхронные операции (отправка email, индексация в поиске, запись в журнал событий):** + - Не влияют на основной бизнес-процесс + - Могут быть отложены без ущерба для пользователя + - Продавец может получить письмо с задержкой + - Поиск может обновиться через 1-2 секунды + +3. **Как обеспечить идемпотентность при повторных запросах?** + + Идемпотентность обеспечивается через: + + - **idempotency_key:** Клиент генерирует уникальный ключ (например, "seller-123_2026-03-12_14-30-15") и передаёт в заголовке запроса. При первом запросе ключ сохраняется в таблице `idempotent_requests` вместе с результатом. При повторном запросе с тем же ключом возвращается кэшированный ответ. + + - **Проверка существующих данных:** Перед созданием объявления проверяем, нет ли уже похожего объявления от того же продавца за последние 10 минут. + + - **Уникальные индексы в БД:** Добавляем UNIQUE INDEX на поле `idempotency_key`. + + - **ad_id как _id в Elasticsearch:** При повторной индексации документ просто перезаписывается. + +4. **Что произойдёт, если внешний сервис вернёт ошибку после частичного выполнения операции?** + + В зависимости от типа ошибки: + + - **ML-модератор недоступен:** Объявление уже создано в БД (фото загружены). Система помечает объявление как "PENDING_MODERATION" и ставит задачу в очередь. Фоновый worker повторяет запрос позже. Данные не теряются. + + - **Image Store недоступен:** Транзакция полностью откатывается. Объявление не создаётся. Фронтенд сохраняет фото локально для последующей синхронизации. + + - **БД недоступна:** Вся транзакция откатывается (ROLLBACK). Если включён offline-режим, данные сохраняются локально для последующей синхронизации. + +5. **Как система обнаружит, что внешний сервис недоступен?** + + - **Таймауты:** HTTP-клиент настроен с таймаутами (3 сек для ML, 5 сек для Image Store). При превышении таймаута генерируется TimeoutException. + + - **Статус-коды HTTP:** При получении 5xx ошибок (503 Service Unavailable, 500 Internal Server Error) система интерпретирует это как недоступность сервиса. + + - **Health checks:** Система периодически проверяет доступность внешних сервисов через эндпоинты /health. + + - **Circuit Breaker:** При определённом количестве ошибок цепь размыкается, и запросы направляются в очередь retry. + +6. **Какие данные нужно логировать для диагностики сбоев?** + + - **Идентификаторы:** request_id, ad_id, seller_id, session_id + - **Временные метки:** timestamp начала и окончания операции, длительность выполнения + - **Данные запроса:** endpoint, HTTP метод, заголовки (с маскированием sensitive data) + - **Информация об ошибке:** тип исключения, код ошибки, сообщение, stack trace + - **Контекст выполнения:** название сервиса, версия приложения, окружение + - **Данные ответа:** статус-код, тело ответа (для неудачных запросов) + - **Метрики производительности:** latency, количество попыток retry, размер очереди + +--- + +## Ссылка на репозиторий + +👉 **GitHub:** _[[репозиторий]](https://github.com/kerubifi/PIS-2026)_ + +--- + +## Вывод + +В ходе выполнения лабораторной работы был проанализирован бизнес-процесс публикации объявления на доске объявлений «Бери, пока горячее». Разработано use-case описание, построены диаграммы последовательности (happy path и error case), созданы 5 Gherkin-сценариев, проведён анализ границ ответственности с таблицей транзакционных границ и стратегиями обработки исключительных ситуаций (retry с exponential backoff, компенсация, уведомления). + +**Освоенные навыки:** моделирование бизнес-процессов, построение UML-диаграмм в PlantUML, написание Gherkin-сценариев, проектирование отказоустойчивости (идемпотентность, retry-стратегии, circuit breaker). + +**Сложности:** проектирование retry-стратегии для ML-модератора. Решение: объявление сохраняется в любом случае, при сбое — статус `PENDING_MODERATION` и очередь повторных попыток (30с, 1м, 2м, 5м, 15м). + +**Новые знания:** углублённое понимание транзакционных границ, идемпотентности в распределённых системах, практические навыки PlantUML и Gherkin, компромиссы CAP theorem. + + +--- + +**Дата выполнения:** _06.05_ + +**Оценка:** _____________ + +**Подпись преподавателя:** _____________ diff --git a/students/Tsuytskou_Kiryl/lab_01/analysis.md b/students/Tsuytskou_Kiryl/lab_01/analysis.md new file mode 100644 index 00000000..3ec60148 --- /dev/null +++ b/students/Tsuytskou_Kiryl/lab_01/analysis.md @@ -0,0 +1,401 @@ +# Анализ границ ответственности + +**Система:** Доска объявлений «Бери, пока горячее» +**Сценарий:** Публикация объявления + +--- + +## 1. Транзакционные границы + +### 1.1. Где начинается и заканчивается транзакция? + +**Начало транзакции (первой):** Продавец нажимает кнопку «Опубликовать» + +**Конец транзакции (успешный сценарий):** +- **Основная транзакция БД №1:** Объявление сохранено со статусом "ACTIVE" +- **Основная транзакция БД №2:** Фото загружены в Image Store + URL сохранены +- **Побочные эффекты (асинхронные):** Email продавцу + индексация в поиске + +**Важно:** У нас две отдельные транзакции: +1. **Создание объявления** - атомарно (данные + фото) +2. **Модерация + индексация** - асинхронно (не блокирует пользователя) + +Если ML-модератор недоступен, объявление сохраняется со статусом `PENDING_MODERATION` и публикуется позже. + +### 1.2. Какие операции должны быть атомарными? + +#### Транзакция №1: Создание объявления + +**Атомарные операции (в одной БД-транзакции):** +1. Валидация входных данных (категория, заголовок, цена, описание) +2. Загрузка фото в Image Store (синхронно, 3 попытки) +3. Создание записи Ad в таблице `ads` со статусом "ACTIVE" (если ML одобрил) +4. Сохранение URL фото в записи объявления + +**Обоснование:** Объявление без фото или без заголовка не имеет смысла. Если загрузка фото не удалась - всё должно откатиться. + +**Пример сбоя:** +- Если фото загрузились, но создание записи в БД не удалось → нужно удалить загруженные фото +- Иначе получим "фантомные фото" в хранилище без привязки к объявлению + +#### Транзакция №2: Асинхронная модерация + +**Атомарные операции (в одной БД-транзакции):** +1. Проверка статуса объявления (не должно быть уже одобрено) +2. Обновление статуса объявления на "ACTIVE" или "REJECTED" +3. Сохранение причины отклонения (если есть) +4. Добавление в очередь индексации поиска + +**Обоснование:** Модерация не должна блокировать пользователя. Объявление может существовать в статусе ожидания. + +### 1.3. Какие операции могут быть асинхронными? + +| Операция | Обоснование | +|----------|-------------| +| Отправка email продавцу с подтверждением | Может быть отложена. Объявление уже создано | +| Индексация в поиске (Elasticsearch) | Eventual consistency. Появится через 1-2 сек | +| Отправка на ручную модерацию | Пользователь не ждёт. Проверка 5-30 мин | +| Запись событий в журнал (Events Log) | Для аудита, не критично | +| Обновление статистики продавца | Можно пересчитать позже | +| Push-уведомления подписчикам категории | Не влияет на публикацию | + +**Принцип:** Если операция НЕ нарушает инварианты доменной модели при отказе - она может быть асинхронной. + +--- + +## 2. Таблица транзакционных границ + +| Операция | Синхронная/Асинхронная | Откат при ошибке | Retry-стратегия | Идемпотентность | +|----------|------------------------|------------------|-----------------|-----------------| +| **Валидация входных данных (цена, заголовок)** | Синхронная | Нет (просто возврат ошибки) | N/A | Да | +| **Загрузка фото в Image Store** | Синхронная | Да (ROLLBACK транзакции №1) | 3 попытки (1s, 2s, 4s) | Да (по хешу файла) | +| **Проверка ML-модератором** | Синхронная | Нет (перевод в PENDING_MODERATION) | 3 попытки, затем ручная | Да (по ad_id) | +| **Создание записи Ad в БД** | Синхронная | Да (ROLLBACK транзакции №1) | Нет (контролируется БД) | Да (по idempotency_key) | +| **Генерация уникального ad_id** | Синхронная | Нет (детерминированная) | N/A | Да (UUID v4) | +| **Сохранение URL фото в БД** | Синхронная | Да (в той же транзакции №1) | Нет | Да | +| **Публикация доменных событий (AdCreated)** | Синхронная (outbox) | Да (в той же транзакции) | Нет | Да (по event_id) | +| **Отправка email продавцу** | Асинхронная (через очередь) | Нет (объявление уже создано) | 5 попыток (1min, 5min, 15min, 1h, 6h) | Да (по ad_id + email) | +| **Индексация в Elasticsearch** | Асинхронная | Нет (eventual consistency) | 3 попытки с задержкой 1s | Да (по ad_id) | +| **Запись в журнал событий** | Асинхронная (event sourcing) | Нет (best-effort) | 3 попытки | Да (по event_id) | +| **Обновление кэша статистики (Redis)** | Асинхронная | Нет (eventual consistency) | 3 попытки | Да | + +--- + +## 3. Обработка отказов внешних сервисов + +### 3.1. ML-модератор недоступен (Anti-spam API) + +**Проблема:** Сервис проверки текста не отвечает (таймаут, 503 Service Unavailable) + +**Стратегия:** +1. **Таймаут:** 3 секунды (для синхронного вызова) +2. **Основная транзакция:** НЕ откатывается, объявление создаётся со статусом "PENDING_MODERATION" +3. **Retry:** + - Помещаем задачу в очередь `moderation_queue` с типом "ML_TIMEOUT" + - Фоновый worker пытается проверить повторно + - Retry schedule: через 30 секунд, затем 1 минута, 2 минуты, 5 минут, 15 минут + - После 5 неудачных попыток → объявление отправляется живому модератору + - Отправляем уведомление администратору: "ML-модератор недоступен, накоплено X задач" +4. **Компенсация:** Нет (объявление уже создано в PENDING_MODERATION) +5. **Уведомление пользователя:** "Объявление отправлено на проверку. Обычно это занимает 5 минут." + +**Обоснование:** ML-модератор - полезный, но не критичный компонент. Временные сбои не должны блокировать создание объявления. + +### 3.2. Хранилище изображений недоступно (Image Store) + +**Проблема:** S3/MinIO не отвечает (таймаут, 503 Service Unavailable) + +**Стратегия:** +1. **Таймаут:** 5 секунд для загрузки одного фото +2. **Основная транзакция:** ПОЛНОСТЬЮ откатывается +3. **Retry:** + - На уровне приложения: 3 попытки с экспоненциальной задержкой (1s, 2s, 4s) + - Если все 3 попытки не удались → возвращаем 503 Service Unavailable +4. **Компенсация:** Удаляем уже загруженные фото (если частично успели) +5. **Offline-режим:** + - Фронтенд сохраняет фото в IndexedDB/LocalStorage + - При восстановлении связи → синхронизация с сервером +6. **Уведомление пользователя:** "Не удалось загрузить фото. Попробуйте позже или выберите другие изображения." + +**Обоснование:** Фото - критичная часть объявления. Без них объявление бесполезно. + +### 3.3. База данных недоступна + +**Проблема:** PostgreSQL не отвечает (connection refused, timeout) + +**Стратегия:** +1. **Таймаут:** 5 секунд для connection, 30 секунд для query +2. **Основная транзакция:** ROLLBACK (объявление НЕ создаётся) +3. **Retry:** + - На уровне приложения: 3 попытки с экспоненциальной задержкой (1s, 2s, 4s) + - Если все 3 попытки не удались → возвращаем 503 Service Unavailable +4. **Компенсация:** Нет (ничего не было создано) +5. **Offline-режим:** + - Сохраняем объявление локально в IndexedDB + - При восстановлении связи → синхронизация с сервером +6. **Уведомление пользователя:** "Не удалось создать объявление. Попробуйте ещё раз." + +**Обоснование:** БД - критичный компонент. Без неё объявление не может существовать. + +### 3.4. Поисковый движок недоступен (Elasticsearch) + +**Проблема:** Elasticsearch не отвечает при индексации + +**Стратегия:** +1. **Таймаут:** 2 секунды +2. **Основная транзакция:** НЕ откатывается (объявление уже в БД) +3. **Retry:** + - Помещаем задачу в очередь `search_index_queue` + - Фоновый worker повторяет индексацию + - Retry schedule: через 30 секунд, 1 минута, 2 минуты, 5 минут + - После 4 неудачных попыток → алерт администратору +4. **Компенсация:** Нет (объявление существует в БД, поиск обновится позже) +5. **Уведомление пользователя:** Никакого (пользователь не знает про поиск) + +**Обоснование:** Поиск - read-модель. Временная рассинхронизация допустима (eventual consistency). + +### 3.5. Конкурентная публикация одного товара + +**Проблема:** Два разных продавца одновременно публикуют одинаковые объявления + +**Стратегия:** +1. **Блокировка:** НЕ используем (разные продавцы могут продавать одинаковые товары) +2. **Проверка дубликатов для ОДНОГО продавца:** + ```sql + SELECT * FROM ads + WHERE seller_id = 'USER-123' + AND title = 'iPhone 13' + AND created_at > NOW() - INTERVAL '10 minutes' + FOR UPDATE; +3. **Основная транзакция:** Откат для второго запроса того же продавца +4. **Уведомление пользователя:** "Похожее объявление уже создано 5 минут назад. Проверьте статус." + +**Обоснование:** Оптимистичная проверка только для дубликатов от одного продавца. + +--- + +## 4. Идемпотентность + +### 4.1. Проблема: Повторное нажатие кнопки "Опубликовать" + +**Сценарий:** Продавец дважды кликнул на кнопку из-за медленного интернета + +**Решение:** + +1. **Frontend:** Блокировать кнопку после первого клика (disable button) +2. **Backend:** Проверять `idempotency_key` в запросе + +```json +{ + "title": "iPhone 13", + "price": 45000, + "idempotency_key": "seller-123_2026-03-12_14-30-15" +} + ``` +3. **Логика:** + - При получении запроса проверяем таблицу `idempotent_requests` + - Если `idempotency_key` уже есть → возвращаем кэшированный ответ (ad_id) + - Если нет → обрабатываем запрос и сохраняем в `idempotent_requests` +4. **TTL ключей:** 24 часа + +**Пример таблицы:** +```sql +CREATE TABLE idempotent_requests ( + idempotency_key VARCHAR(128) PRIMARY KEY, + endpoint VARCHAR(255) NOT NULL, + response_body JSONB NOT NULL, + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX idx_idempotent_created ON idempotent_requests(created_at); +-- Очистка старых записей по cron (старше 24 часов) +``` + +### 4.2. Проблема: Дубликаты email-уведомлений + +**Сценарий:** Notification Service получил запрос дважды из-за retry + +**Решение:** +1. В таблице `notification_queue` добавляем уникальный ключ: + ```sql + CREATE UNIQUE INDEX idx_notification_dedup + ON notification_queue(request_id, group_id, recipient_phone, notification_type); + ``` +2. При попытке добавить дубликат → игнорируем (ON CONFLICT DO NOTHING) + +### 4.3. Проблема: Повторное создание объявления с теми же данными + +**Сценарий:** Продавец дважды нажал "Опубликовать" из-за задержки ответа сервера + +**Решение:** +1. Проверяем, есть ли уже объявление с таким же title, seller_id и датой создания (в пределах 10 минут): + ```sql + SELECT * FROM ads + WHERE seller_id = 'USER-123' + AND title = 'iPhone 13' + AND created_at > NOW() - INTERVAL '10 minutes' + AND status != 'REJECTED'; + ``` +2. Если объявление уже существует → возвращаем его ID (не создаём новое) +3. Уведомление: "Похожее объявление уже создано. ID: A-2026-0142 + +--- + +## 5. Диаграмма потока данных + +``` +┌────────────────┐ +│ Seller │ +└───────┬────────┘ + │ 1. POST /api/ads + idempotency_key + ▼ +┌───────────────────────┐ +│ API Gateway │ +└───────┬───────────────┘ + │ 2. Forward request + ▼ +┌───────────────────────┐ +│ Ad Service │──────┐ +└───────┬───────────────┘ │ + │ 3. Validate data │ + ▼ │ +┌───────────────────────┐ │ +│ Image Store │ │ +└───────┬───────────────┘ │ + │ 4. Upload images │ + ▼ │ +┌───────────────────────┐ │ +│ ML Moderator │ │ +└───────┬───────────────┘ │ + │ 5. Check content │ + ▼ │ +┌───────────────────────┐ │ +│ PostgreSQL │◄─────┘ +│ tables: ads │ 6. COMMIT +└───────┬───────────────┘ + │ 7. Ad created + ▼ +┌───────────────────────┐ +│ Event Bus │ +└───────┬───────────────┘ + │ + ├──────────────┬──────────────┐ + ▼ ▼ ▼ +┌──────────────┐ ┌──────────────┐ ┌──────────────┐ +│ Search │ │ Notification │ │ Events Log │ +│ Worker │ │ Worker │ │ Worker │ +└──────┬───────┘ └──────┬───────┘ └──────────────┘ + ▼ ▼ +┌──────────────┐ ┌──────────────┐ +│ Elasticsearch│ │ Email/Push │ +└──────────────┘ └──────────────┘ +``` + +--- + +## 6. Метрики и мониторинг + +**Ключевые метрики для отслеживания:** + +| Метрика | Цель | Алерт при | +|---------|------|-----------| +| `ad_creation_latency_p95` | < 1 секунда | > 3 секунд | +| `ad_creation_success_rate` | > 99% | < 98% | +| `image_upload_latency_p95` | < 3 секунды | > 6 секунд | +| `image_upload_success_rate` | > 99% | < 97% | +| `ml_moderation_latency_p95` | < 1 секунда | > 3 секунд | +| `ml_moderation_timeout_count` | 0 | > 10/hour | +| `db_connection_pool_active` | < 80% от макс. | > 90% | +| `moderation_queue_size` | < 500 | > 2000 | +| `email_delivery_rate` | > 98% в течение 5 минут | < 95% | +| `search_index_lag` | < 2 секунды | > 10 секунд | +| `pending_moderation_count` | < 100 | > 500 | +| `offline_sync_pending_count` | 0 | > 50 | +| `idempotency_key_usage_rate` | < 5% запросов | > 10% | + +**Логирование:** +- **INFO:** Успешное создание объявления (ad_id, seller_id) +- **INFO:** Загрузка фото (количество, размеры) +- **INFO:** Результат ML-модерации (verdict, confidence) +- **WARNING:** ML-модератор timeout, объявление в PENDING_MODERATION +- **WARNING:** Image Store timeout, откат транзакции +- **WARNING:** Очередь модерации превысила 500 +- **ERROR:** БД недоступна, откатили транзакцию +- **ERROR:** Image Store вернул ошибку после 3 retry +- **CRITICAL:** Очередь уведомлений переполнена (> 5000) +- **CRITICAL:** Более 10% объявлений уходят в PENDING_MODERATION из-за ML-сбоев + +--- + +## 7. Выводы + +### Ключевые решения: + +1. ✅ **Одна транзакция:** Создание объявления происходит атомарно вместе с загрузкой фото и ML-проверкой. +2. ✅ **Атомарность:** Объявление создаётся в одной транзакции; при сбое загрузки фото - полный откат. +3. ✅ **Асинхронность:** Email-уведомления, индексация в поиске и отложенная модерация вынесены в очередь, что ускоряет публикацию и обеспечивает отказоустойчивость. +4. ✅ **Идемпотентность:** Использование `idempotency_key` для дедупликации запросов предотвращает создание дубликатов объявлений при повторных нажатиях или retry. +5. ✅ **Pessimistic Locking:** Применение `SELECT ... FOR UPDATE` для проверки дубликатов от одного продавца. +6. ✅ **Отказоустойчивость:** + - Объявление не откатывается при сбое ML-модератора (статус `PENDING_MODERATION`) + - Offline-режим позволяет продавцам работать при плохом интернете с последующей синхронизацией +7. ✅ **Retry-стратегии:** + - Exponential backoff для временных сбоев (1с, 2с, 4с для фото; 30с, 1м, 2м, 5м, 15м для ML) + - После 5 неудачных попыток - алерт администратора и ручная модерация +8. ✅ **Мониторинг:** Система метрик для каждого критичного компонента с порогами алертов позволяет оперативно реагировать на проблемы. +9. ✅ **Eventual Consistency:** Индексация в поиске происходит асинхронно, что не влияет на публикацию. + +### Транзакционные инварианты: + +- **Инвариант №1:** Объявление без заголовка или категории недействительно → откатываем транзакцию +- **Инвариант №2:** Цена должна быть положительным числом → CHECK constraint в БД +- **Инвариант №3:** У объявления должна быть хотя бы одна фотография → проверка перед созданием +- **Инвариант №4:** Один продавец не может создать два одинаковых объявления за 10 минут → проверка дубликатов +- **Инвариант №5:** После удаления объявления все ссылки должны возвращать 410 Gone + +### Компромиссы: + +- **Eventual consistency для поиска:** Объявление может появиться в поиске через 1-2 секунды после публикации, что допустимо для доски объявлений. +- **Асинхронная модерация:** При сбое ML пользователь видит статус "На модерации", объявление публикуется с задержкой 5-30 минут. +- **Хранение idempotency keys 24 часа:** Занимает дополнительное место в БД (около 500 МБ в месяц при 5000 объявлений/день), но предотвращает дубликаты. +- **Одна транзакция вместо двух:** Меньше гибкости (нельзя сохранить черновик без фото), но проще атомарность. +- **Pessimistic locking для дубликатов:** Блокировка строки при проверке, но дубликаты от одного продавца случаются редко. + +### Риски и их митигация: + +⚠️ **Риск 1:** Очередь модерации может переполниться при массовой публикации (например, в выходные дни) +- **Митигация:** Установить лимит очереди 5000 сообщений, алерт при превышении 80%, авто-масштабирование воркеров + +⚠️ **Риск 2:** Image Store может быть недоступен длительное время, накапливая очередь retry +- **Митигация:** Offline-режим на фронте, после 3 неудачных попыток - алерт администратору + +⚠️ **Риск 3:** ML-модератор может ошибаться (false positive - хорошее объявление отклонено) +- **Митигация:** Отправка false positive на дообучение модели + ручная модерация с возможностью апелляции + +⚠️ **Риск 4:** Продавец может создать дублирующиеся объявления в обход проверки +- **Митигация:** Проверка по хешу заголовка + хешу фото (pHash) через ML + +⚠️ **Риск 5:** Потеря данных при offline-синхронизации +- **Митигация:** Использовать local storage с versioning, конфликт резолвинг по принципу "последняя запись побеждает" + +⚠️ **Риск 6:** Утечка персональных данных при логировании +- **Митигация:** Маскировать email и телефон в логах, использовать шифрование при хранении в БД + +### Достигнутые цели: + +1. **Надёжность:** Система сохраняет объявления даже при сбоях ML-модератора и Image Store +2. **Целостность:** Транзакционные инварианты защищают от некорректных объявлений (отрицательная цена, пустой заголовок) +3. **Масштабируемость:** Асинхронная обработка позволяет обрабатывать пиковые нагрузки (до 7000 объявлений/день) +4. **Прозрачность:** Метрики и логи позволяют отслеживать состояние системы и быстро реагировать на проблемы +5. **Удобство использования:** Offline-режим и идемпотентность делают публикацию комфортной даже при нестабильном интернете + +### Рекомендации по улучшению (следующие версии): + +1. **Авто-обновление статуса "Продано":** При получении подтверждения от платёжного шлюза (если интеграция с оплатой есть) +2. **Буст объявлений:** Платное поднятие в топ с автоматическим списанием средств +3. **История цен:** Отображение изменения цены для покупателей ("было 5000, стало 4000") +4. **Геолокация:** Фильтр объявлений по радиусу + интеграция с картами +5. **Чат между продавцом и покупателем:** WebSocket для реал-тайм общения +6. **Повторная модарация:** Возможность исправить отклонённое объявление и отправить на повторную проверку +7. **Автоматическая архивация:** Через 90 дней объявление переводится в статус "ARCHIVED" +8. **Избранное:** Redis для быстрых операций с избранными объявлениями \ No newline at end of file diff --git a/students/Tsuytskou_Kiryl/lab_01/diagrams/sequence-error-payment.png b/students/Tsuytskou_Kiryl/lab_01/diagrams/sequence-error-payment.png new file mode 100644 index 00000000..9ecd13e1 Binary files /dev/null and b/students/Tsuytskou_Kiryl/lab_01/diagrams/sequence-error-payment.png differ diff --git a/students/Tsuytskou_Kiryl/lab_01/diagrams/sequence-error-payment.puml b/students/Tsuytskou_Kiryl/lab_01/diagrams/sequence-error-payment.puml new file mode 100644 index 00000000..cc4d69f4 --- /dev/null +++ b/students/Tsuytskou_Kiryl/lab_01/diagrams/sequence-error-payment.puml @@ -0,0 +1,183 @@ +@startuml +title Сценарий с ошибкой: Таймаут ML-модератора (Anti-spam API недоступен) + +actor "Продавец" as Seller +participant "Web UI" as UI +participant "API Gateway" as Gateway +participant "Ad Service" as AdSvc +participant "Image Store\n(S3/MinIO)" as ImgStore +database "PostgreSQL" as DB +participant "ML Moderator\n(Anti-spam API)" as ML +queue "Event Bus\n(RabbitMQ)" as Bus +participant "Moderation Retry\nWorker" as RetryWorker +participant "Notification\nService" as Notify + +note over Seller, Notify + Шаг 1: Пользователь заполнил форму объявления + и нажал "Опубликовать" +end note + +== Транзакция №1: Загрузка фото и сохранение объявления == + +Seller -> UI: Заполняет форму\n(категория, заголовок, цена, описание, 3 фото) +UI -> UI: Валидация на фронте + +Seller -> UI: Нажать "Опубликовать" +UI -> Gateway: POST /api/ads\nmultipart/form-data +activate Gateway + +Gateway -> Gateway: Аутентификация\nи верификация пользователя +Gateway -> AdSvc: Пересылка запроса +activate AdSvc + +AdSvc -> AdSvc: Валидация данных\n(цена > 0, заголовок не пуст,\nкатегория существует) + +== Параллельная загрузка изображений == + +AdSvc -> ImgStore: upload_image(file1, file2, file3) +activate ImgStore +ImgStore --> AdSvc: [url1, url2, url3] +deactivate ImgStore + +note over AdSvc + Фото успешно загружены + Теперь нужно проверить текст через ML-модератора +end note + +== Шаг 2: Ошибка при обращении к ML-модератору == + +AdSvc -> ML: checkContent(title, description, category) +activate ML + +note over ML #FFAAAA + ⚠️ ML-модератор НЕ ДОСТУПЕН + Timeout 3 секунды / 503 Service Unavailable +end note + +ML --x AdSvc: TimeoutException / HTTP 503 +deactivate ML + +AdSvc -> AdSvc: Перехват исключения + +note over AdSvc + Система НЕ откатывает транзакцию + Фото уже загружены + Объявление будет сохранено со статусом PENDING_MODERATION +end note + +AdSvc -> DB: BEGIN TRANSACTION +activate DB + +AdSvc -> DB: INSERT INTO ads\n(id, seller_id, title, price,\n category, description, images,\n status, created_at)\nVALUES ('A-2026-0142', ...)\nstatus = 'PENDING_MODERATION' +DB --> AdSvc: OK + +AdSvc -> DB: INSERT INTO moderation_queue\n(ad_id, reason, priority, created_at)\nVALUES ('A-2026-0142',\n 'ML_TIMEOUT', 'HIGH', NOW()) +DB --> AdSvc: OK + +AdSvc -> DB: INSERT INTO outbox\n(event_type='AdPendingModeration',\n payload=..., status='pending') +DB --> AdSvc: OK + +AdSvc -> DB: COMMIT TRANSACTION +DB --> AdSvc: Committed +deactivate DB + +AdSvc -> Bus: publish(AdPendingModeration,\nad_id='A-2026-0142',\nreason='ML_TIMEOUT') +activate Bus +Bus --> AdSvc: ACK +deactivate Bus + +note right of AdSvc #FFFFAA + Объявление в статусе PENDING_MODERATION + Задача в очереди для повторной проверки модератором +end note + +AdSvc --> Gateway: 202 Accepted\n{ad_id: "A-2026-0142",\nstatus: "PENDING_MODERATION",\nmessage: "Проверка антисписком задерживается. Объявление появится автоматически"} +deactivate AdSvc + +Gateway --> UI: 202 Accepted\n{ad_id, status, message} +deactivate Gateway + +UI --> Seller: Показать уведомление:\n"Объявление отправлено на проверку. Обычно это занимает 5 минут" + +note over Seller + Продавец получил предупреждение + Объявление сохранено, но пока не видно другим пользователям +end note + +== Асинхронная обработка ошибки (Retry через 30 секунд) == + +Bus -> RetryWorker: consume(AdPendingModeration) +activate RetryWorker + +RetryWorker -> RetryWorker: Задержка 30 секунд\n(первая попытка retry к ML) + +... 30 секунд спустя ... + +RetryWorker -> ML: checkContent(title, description, category) +activate ML + +note over ML #AAFFAA + ✅ ML-модератор снова доступен + Успешная проверка текста +end note + +ML --> RetryWorker: verdict = "CLEAN"\nconfidence = 0.96\nreason = NULL +deactivate ML + +RetryWorker -> DB: BEGIN TRANSACTION +activate DB + +RetryWorker -> DB: UPDATE ads\nSET status='ACTIVE',\nmoderation_verdict='CLEAN',\nmoderated_at=NOW()\nWHERE id='A-2026-0142' +DB --> RetryWorker: OK + +RetryWorker -> DB: UPDATE moderation_queue\nSET status='PROCESSED',\nresolved_at=NOW()\nWHERE ad_id='A-2026-0142' +DB --> RetryWorker: OK + +RetryWorker -> DB: INSERT INTO search_index_queue\n(ad_id, action, queued_at)\nVALUES ('A-2026-0142', 'INDEX', NOW()) +DB --> RetryWorker: OK + +RetryWorker -> DB: UPDATE outbox\nSET status='sent'\nWHERE event_type='AdPendingModeration'\nAND payload->>'ad_id'='A-2026-0142' +DB --> RetryWorker: OK + +RetryWorker -> DB: COMMIT TRANSACTION +deactivate DB + +RetryWorker -> Bus: publish(AdPublished,\nad_id='A-2026-0142',\nseller_id='USER-123') +activate Bus +Bus --> RetryWorker: ACK +deactivate Bus + +note over RetryWorker #AAFFAA + Объявление успешно проверено + Теперь активно и доступно в поиске +end note + +deactivate RetryWorker + +== Асинхронная отправка уведомлений продавцу == + +Bus -> NotificationWorker: consume(AdPublished) +activate NotificationWorker + +NotificationWorker -> NotificationWorker: Получить данные продавца\n(email, телефон) по seller_id + +NotificationWorker -> Notify: sendEmail(to="seller@example.com",\ntemplate="ad_published",\ndata={ad_id: "A-2026-0142",\ntitle: "iPhone 13",\nurl: "/ads/A-2026-0142"}) +activate Notify + +Notify -> Notify: Формирование и отправка\nписьма через Email-провайдера +Notify --> NotificationWorker: Status: sent\nMessage-ID: msg-789 +deactivate Notify + +NotificationWorker -> DB: INSERT INTO notification_log\n(ad_id, recipient, type, status, sent_at)\nVALUES ('A-2026-0142',\n'seller@example.com', 'email', 'delivered', NOW()) +activate DB +DB --> NotificationWorker: OK +deactivate DB + +note over NotificationWorker #AAFFAA + Email успешно отправлен продавцу + Продавец знает, что объявление опубликовано +end note + +deactivate NotificationWorker + +@enduml \ No newline at end of file diff --git a/students/Tsuytskou_Kiryl/lab_01/diagrams/sequence-happy.png b/students/Tsuytskou_Kiryl/lab_01/diagrams/sequence-happy.png new file mode 100644 index 00000000..9dc1cae0 Binary files /dev/null and b/students/Tsuytskou_Kiryl/lab_01/diagrams/sequence-happy.png differ diff --git a/students/Tsuytskou_Kiryl/lab_01/diagrams/sequence-happy.puml b/students/Tsuytskou_Kiryl/lab_01/diagrams/sequence-happy.puml new file mode 100644 index 00000000..cd257119 --- /dev/null +++ b/students/Tsuytskou_Kiryl/lab_01/diagrams/sequence-happy.puml @@ -0,0 +1,120 @@ +@startuml +title Успешный сценарий: Публикация объявления + +actor "Продавец" as Seller +participant "Web UI" as UI +participant "API Gateway" as Gateway +participant "Ad Service" as AdSvc +participant "ML Moderator" as ML +participant "Image Store\n(S3/MinIO)" as ImgStore +database "PostgreSQL" as DB +queue "Event Bus\n(RabbitMQ)" as Bus +participant "Search Worker" as SearchWorker +participant "Notification\nWorker" as NotifyWorker +participant "Email/Push\nService" as Notify + +== Транзакция №1: Создание объявления == + +Seller -> UI: Заполняет форму объявления\n(категория, заголовок, цена, описание) +activate UI + +UI -> UI: Валидация на фронте\n(цена > 0, заголовок не пуст) + +Seller -> UI: Выбирает 3 фото для загрузки +UI -> UI: Сжимает фото перед отправкой + +UI -> Gateway: POST /api/ads\nmultipart/form-data\n{title: "iPhone 13",\nprice: 45000,\ncategory: "Электроника",\nimages: [file1, file2, file3]} +activate Gateway + +Gateway -> Gateway: Аутентификация\nи верификация пользователя +Gateway -> AdSvc: Пересылка запроса +activate AdSvc + +AdSvc -> AdSvc: Валидация данных\n(цена, категория, длина заголовка) + +== Параллельная загрузка изображений == + +AdSvc -> ImgStore: upload_image(file1) +activate ImgStore +ImgStore --> AdSvc: https://cdn.ads.by/img/abc123.jpg +deactivate ImgStore + +AdSvc -> ImgStore: upload_image(file2) +activate ImgStore +ImgStore --> AdSvc: https://cdn.ads.by/img/def456.jpg +deactivate ImgStore + +AdSvc -> ImgStore: upload_image(file3) +activate ImgStore +ImgStore --> AdSvc: https://cdn.ads.by/img/ghi789.jpg +deactivate ImgStore + +== Синхронная модерация (ML) == + +AdSvc -> ML: checkContent(title, description,\ncategory) +activate ML +ML -> ML: Анализ на спам,\nзапрещённые слова,\nмошенничество +ML --> AdSvc: verdict = "CLEAN"\nconfidence = 0.97 +deactivate ML + +AdSvc -> DB: BEGIN TRANSACTION +activate DB + +AdSvc -> DB: INSERT INTO ads\n(id, seller_id, title, price,\n category, description, status,\n images, created_at)\nVALUES ('A-2026-0142', ...)\nstatus = 'ACTIVE' +DB --> AdSvc: OK + +AdSvc -> DB: INSERT INTO search_index_queue\n(ad_id, action, queued_at)\nVALUES ('A-2026-0142', 'INDEX', NOW()) +DB --> AdSvc: OK + +AdSvc -> DB: INSERT INTO outbox\n(event_type='AdPublished',\n payload=..., status='pending') +DB --> AdSvc: OK + +AdSvc -> DB: COMMIT TRANSACTION +DB --> AdSvc: Committed +deactivate DB + +AdSvc --> Gateway: 201 Created\n{ad_id: "A-2026-0142",\nstatus: "ACTIVE",\nurl: "/ads/A-2026-0142"} +deactivate AdSvc + +Gateway --> UI: 201 Created\n{ad_id, status, url} +deactivate Gateway + +UI --> Seller: Показывает карточку\n"Объявление опубликовано!" +deactivate UI + +== Асинхронная индексация в поиске == + +Bus -> SearchWorker: consume(AdPublished) +activate SearchWorker + +SearchWorker -> SearchWorker: Формирование\nElasticsearch документа + +SearchWorker -> SearchWorker: Индексация\n(шард по категории) + +SearchWorker -> DB: UPDATE outbox\nSET status='sent'\nWHERE event_type='AdPublished' +activate DB +DB --> SearchWorker: OK +deactivate DB + +deactivate SearchWorker + +== Асинхронная отправка уведомлений == + +Bus -> NotifyWorker: consume(AdPublished) +activate NotifyWorker + +NotifyWorker -> NotifyWorker: Определение каналов\n(email для продавца) + +NotifyWorker -> Notify: sendEmail(seller@email.com,\n"Ваше объявление A-2026-0142 опубликовано!") +activate Notify +Notify --> NotifyWorker: Message-ID: msg-456\nStatus: queued +deactivate Notify + +NotifyWorker -> DB: INSERT INTO notification_log\n(ad_id, recipient, type, status)\nVALUES ('A-2026-0142', ...) +activate DB +DB --> NotifyWorker: OK +deactivate DB + +deactivate NotifyWorker + +@enduml \ No newline at end of file diff --git a/students/Tsuytskou_Kiryl/lab_01/scenarios.feature b/students/Tsuytskou_Kiryl/lab_01/scenarios.feature new file mode 100644 index 00000000..f9dcf88c --- /dev/null +++ b/students/Tsuytskou_Kiryl/lab_01/scenarios.feature @@ -0,0 +1,623 @@ +Feature: Публикация объявления на доске «Бери, пока горячее» + Как Продавец + Я хочу быстро размещать объявления о продаже товаров + Чтобы находить покупателей и продавать вещи + + Background: + Given пользователь авторизован как "seller@example.com" с ролью "Продавец" + And пользователь подтвердил email "seller@example.com" + And в системе есть категория "Электроника" + And сервис модерации (ML) доступен + And хранилище изображений доступно + + # ======================================== + # УСПЕШНЫЙ СЦЕНАРИЙ (Happy Path) + # ======================================== + + Scenario: Успешная публикация объявления + Given продавец хочет продать товар "iPhone 13 128GB" + And указывает следующие данные: + | Поле | Значение | + | Категория | Электроника | + | Заголовок | iPhone 13 128GB синий, б/у | + | Цена | 45000 | + | Описание | В идеальном состоянии, чехол и зарядка в подарок | + When продавец загружает 3 фотографии телефона + And нажимает кнопку "Опубликовать" + Then система проверяет обязательные поля + And система загружает фото в хранилище и получает URL + And система проверяет текст через ML-модератора + And ML-модератор возвращает вердикт "CLEAN" + And система создаёт объявление с уникальным ID "A-2026-0142" + And объявление имеет статус "ACTIVE" + And система публикует событие "AdPublished" + + And система отправляет email продавцу "seller@example.com" с темой: + """ + Ваше объявление A-2026-0142 опубликовано! + """ + And email содержит ссылку на просмотр объявления + + And продавец видит сообщение: + """ + Объявление успешно опубликовано! Ждём покупателей. + """ + And в личном кабинете отображается объявление со статусом "Активно" + + # ======================================== + # СЦЕНАРИИ С ОШИБКАМИ ВАЛИДАЦИИ + # ======================================== + + Scenario: Ошибка - цена объявления отрицательная + When продавец создаёт объявление со следующими данными: + | Поле | Значение | + | Категория | Электроника | + | Заголовок | iPhone 13 | + | Цена | -500 | + | Описание | Отдам даром за 500 рублей | + And нажимает "Опубликовать" + Then система проверяет цену на положительное значение + And система возвращает ошибку с кодом "INVALID_PRICE" + And сообщение содержит "Цена должна быть положительным числом" + And объявление НЕ создаётся + And поле "Цена" подсвечено красным + And продавец видит подсказку: + """ + Цена не может быть отрицательной. Укажите корректную сумму. + """ + + Scenario: Ошибка - заголовок слишком короткий + When продавец создаёт объявление со следующими данными: + | Поле | Значение | + | Категория | Электроника | + | Заголовок | Телефон | + | Цена | 45000 | + | Описание | Продаю iPhone 13 | + And нажимает "Опубликовать" + Then система проверяет длину заголовка (минимум 10 символов) + And система возвращает ошибку с кодом "TITLE_TOO_SHORT" + And сообщение содержит "Заголовок должен содержать минимум 10 символов" + And объявление НЕ создаётся + And поле "Заголовок" подсвечено красным + + Scenario: Ошибка - категория не выбрана (обязательное поле) + When продавец создаёт объявление со следующими данными: + | Поле | Значение | + | Категория | | + | Заголовок | iPhone 13 128GB б/у | + | Цена | 45000 | + | Описание | Отличный телефон | + And нажимает "Опубликовать" + Then система проверяет обязательные поля + And система обнаруживает, что поле "Категория" не заполнено + And система возвращает ошибку с кодом "REQUIRED_FIELD_MISSING" + And сообщение содержит "Выберите категорию товара" + And объявление НЕ создаётся + And поле "Категория" подсвечено красным + + Scenario: Ошибка - не загружено ни одного фото + When продавец создаёт объявление с заполненными полями + But не загружает ни одного фото + And нажимает "Опубликовать" + Then система проверяет наличие фотографий + And система возвращает ошибку с кодом "IMAGES_REQUIRED" + And сообщение содержит "Загрузите хотя бы одно фото товара" + And объявление НЕ создаётся + And блок загрузки фото подсвечен красным + + # ======================================== + # СЦЕНАРИИ С ОШИБКАМИ БИЗНЕС-ЛОГИКИ + # ======================================== + + Scenario: Ошибка - дубликат объявления (тот же товар за 10 минут) + Given у продавца уже есть активное объявление "iPhone 13 128GB" созданное 5 минут назад + When продавец создаёт новое объявление с таким же заголовком "iPhone 13 128GB" + And нажимает "Опубликовать" + Then система проверяет наличие дубликатов за последние 10 минут + And система обнаруживает похожее объявление "A-2026-0141" + And система возвращает ошибку с кодом "DUPLICATE_AD" + And сообщение содержит: + """ + Похожее объявление уже создано 5 минут назад. Пожалуйста, проверьте статус. + """ + And новое объявление НЕ создаётся + And продавец видит ссылку на существующее объявление + + Scenario: Ошибка - пользователь не верифицирован + Given пользователь "unverified@example.com" не подтвердил email + When пользователь пытается создать объявление + And нажимает "Опубликовать" + Then система проверяет статус верификации пользователя + And система возвращает ошибку с кодом "USER_NOT_VERIFIED" + And сообщение содержит: + """ + Для публикации объявления необходимо подтвердить email. Перейдите по ссылке в письме. + """ + And объявление НЕ создаётся + + Scenario: Ошибка - объявление не найдено при редактировании + Given объявления с ID "A-999999" не существует в системе + When продавец пытается отредактировать объявление "A-999999" + Then система проверяет существование объявления + And система возвращает ошибку с кодом "AD_NOT_FOUND" + And сообщение содержит "Объявление с ID A-999999 не найдено" + And HTTP ответ имеет статус код 404 (Not Found) + + # ======================================== + # СЦЕНАРИИ СБОЕВ ИНФРАСТРУКТУРЫ + # ======================================== + + Scenario: Таймаут ML-модератора (Anti-spam API недоступен) + Given "ML-модератор" недоступен (таймаут 3 секунды) + When продавец создаёт объявление со всеми данными и загружает фото + And нажимает "Опубликовать" + Then система загружает фото в хранилище и получает URL + And система сохраняет объявление в БД + + When система пытается проверить текст через ML-модератора + Then система получает "TimeoutException" (сервер не отвечает 3 секунды) + And система НЕ откатывает создание объявления + And система сохраняет объявление со статусом "PENDING_MODERATION" + And система создаёт запись в очереди модерации с причиной "ML_TIMEOUT" + And система публикует событие "AdPendingModeration" с retry_count=0 + + And продавец видит предупреждение: + """ + Объявление отправлено на проверку. Обычно это занимает 5 минут. Вы получите уведомление. + """ + And HTTP ответ имеет статус код 202 (Accepted) + + And система ставит задачу в очередь на повторную проверку через 30 секунд + And в базе данных есть запись в таблице "moderation_queue": + | ad_id | reason | retry_count | next_retry_at | + | A-2026-0143 | ML_TIMEOUT | 0 | [текущее время + 30с] | + + Scenario: Таймаут хранилища изображений (Image Store недоступен) + Given "Хранилище изображений" недоступно (таймаут 5 секунд) + When продавец создаёт объявление с 3 фотографиями + And нажимает "Опубликовать" + Then система пытается загрузить фото в хранилище + And система получает "TimeoutException" (хранилище не отвечает 5 секунд) + And система откатывает всю транзакцию + And объявление НЕ создаётся + And событие "AdCreated" НЕ публикуется + + And система логирует ошибку с уровнем "ERROR": + """ + Failed to upload images to S3: Connection timeout + """ + And продавец видит сообщение: + """ + Не удалось загрузить фото. Попробуйте позже или выберите другие изображения. + """ + And HTTP ответ имеет статус код 503 (Service Unavailable) + And фронтенд сохраняет фото в LocalStorage для повторной попытки + + Scenario: База данных недоступна при сохранении объявления + Given "База данных" недоступна (connection refused) + When продавец создаёт объявление со всеми данными + And нажимает "Опубликовать" + Then система пытается сохранить объявление в БД + And система получает "DatabaseConnectionException" + And система откатывает всю транзакцию + And объявление НЕ создаётся + And фото НЕ загружаются (откат) + + And система логирует ошибку с уровнем "ERROR": + """ + Failed to create ad: Database connection failed + """ + And продавец видит сообщение: + """ + Не удалось создать объявление. Попробуйте ещё раз или свяжитесь с техподдержкой. + """ + And HTTP ответ имеет статус код 503 (Service Unavailable) + And все введённые данные остаются в форме (не сбрасываются) + + # ======================================== + # ИДЕМПОТЕНТНОСТЬ И КОНКУРЕНТНОСТЬ + # ======================================== + + Scenario: Повторное нажатие кнопки "Опубликовать" не создаёт дубликат + Given продавец заполнил все поля и загрузил фото + When продавец нажимает кнопку "Опубликовать" + And из-за медленного интернета нажимает кнопку второй раз (double-click) + Then система создаёт только ОДНО объявление + And второй запрос возвращает тот же ad_id что и первый + And система проверяет наличие дубликата по idempotency_key + And в базе данных присутствует только 1 запись для этого объявления + And продавец видит сообщение: + """ + Объявление уже создано. ID: A-2026-0142 + """ + + Scenario: Конкурентная публикация одного товара двумя продавцами + Given два продавца одновременно пытаются опубликовать объявление с одинаковым заголовком "iPhone 13" + When Продавец_1 нажимает "Опубликовать" + And Продавец_2 нажимает "Опубликовать" (одновременно) + Then система обрабатывает оба запроса независимо + And создаются два разных объявления с ID "A-2026-0142" и "A-2026-0143" + And оба объявления успешно публикуются + And система проверяет только дубликаты от ОДНОГО пользователя (не блокирует разных) + And оба продавца видят сообщение об успешной публикации + + # ======================================== + # АЛЬТЕРНАТИВНЫЕ ПОТОКИ + # ======================================== + + Scenario: Сохранение черновика объявления + When продавец начинает создавать объявление + And заполняет только: + | Поле | Значение | + | Категория | Электроника | + | Заголовок | iPhone 13 для обсуждения| + And нажимает кнопку "Сохранить в черновики" + Then система сохраняет черновик объявления со статусом "DRAFT" + And система возвращает сообщение: + """ + Черновик сохранён. Вы можете вернуться к редактированию позже. + """ + And черновик отображается в разделе "Черновики" + And объявление НЕ проходит модерацию + And другие пользователи НЕ видят его в поиске + + When продавец через час открывает черновик + And заполняет поле "Цена: 45000" + And загружает 2 фотографии + And нажимает "Опубликовать" + Then система создаёт полноценное объявление со статусом "ACTIVE" (после проверки) + And черновик удаляется из раздела черновиков + + Scenario: Платное поднятие объявления (буст в топ) + Given продавец создал объявление и оно успешно опубликовано + When продавец открывает карточку объявления + And выбирает опцию "Поднять в топ на 3 дня" + Then система отображает стоимость услуги: "199 рублей" + + When продавец подтверждает оплату + Then система создаёт платёжную ссылку + And после успешной оплаты обновляет объявление: + | Поле | Значение | + | boost_expires_at | NOW() + 3 days | + | priority | HIGH | + And объявление получает приоритет при сортировке + And продавец видит сообщение: + """ + Ваше объявление поднято в топ до 15.03.2026 + """ + + # ======================================== + # ГРАНИЧНЫЕ СЛУЧАИ + # ======================================== + + Scenario: Создание объявления с минимальной ценой + When продавец создаёт объявление с ценой "1.00" RUB + Then система принимает объявление + And объявление создаётся успешно со статусом "ACTIVE" + And объявление отображается в поиске + + Scenario: Создание объявления с максимальной длиной описания + Given максимальная длина описания составляет 5000 символов + When продавец создаёт объявление с описанием ровно на 5000 символов + Then система принимает объявление + And объявление создаётся успешно + + Scenario: Отклонение объявления модератором (после ручной проверки) + Given продавец опубликовал объявление, и ML-модератор отправил его в "PENDING_MODERATION" + When живой модератор проверяет объявление и обнаруживает запрещённый товар (оружие) + Then модератор нажимает "Отклонить" и указывает причину: "Запрещённая категория товаров" + Then система обновляет статус объявления на "REJECTED" + And система отправляет email продавцу с причиной отклонения + And продавец видит в личном кабинете статус "Отклонено: Запрещённая категория товаров" + And продавец может отредактировать объявление и отправить на повторную проверкуFeature: Публикация объявления на доске «Бери, пока горячее» + Как Продавец + Я хочу быстро размещать объявления о продаже товаров + Чтобы находить покупателей и продавать вещи + + Background: + Given пользователь авторизован как "seller@example.com" с ролью "Продавец" + And пользователь подтвердил email "seller@example.com" + And в системе есть категория "Электроника" + And сервис модерации (ML) доступен + And хранилище изображений доступно + + # ======================================== + # УСПЕШНЫЙ СЦЕНАРИЙ (Happy Path) + # ======================================== + + Scenario: Успешная публикация объявления + Given продавец хочет продать товар "iPhone 13 128GB" + And указывает следующие данные: + | Поле | Значение | + | Категория | Электроника | + | Заголовок | iPhone 13 128GB синий, б/у | + | Цена | 45000 | + | Описание | В идеальном состоянии, чехол и зарядка в подарок | + When продавец загружает 3 фотографии телефона + And нажимает кнопку "Опубликовать" + Then система проверяет обязательные поля + And система загружает фото в хранилище и получает URL + And система проверяет текст через ML-модератора + And ML-модератор возвращает вердикт "CLEAN" + And система создаёт объявление с уникальным ID "A-2026-0142" + And объявление имеет статус "ACTIVE" + And система публикует событие "AdPublished" + + And система отправляет email продавцу "seller@example.com" с темой: + """ + Ваше объявление A-2026-0142 опубликовано! + """ + And email содержит ссылку на просмотр объявления + + And продавец видит сообщение: + """ + Объявление успешно опубликовано! Ждём покупателей. + """ + And в личном кабинете отображается объявление со статусом "Активно" + + # ======================================== + # СЦЕНАРИИ С ОШИБКАМИ ВАЛИДАЦИИ + # ======================================== + + Scenario: Ошибка - цена объявления отрицательная + When продавец создаёт объявление со следующими данными: + | Поле | Значение | + | Категория | Электроника | + | Заголовок | iPhone 13 | + | Цена | -500 | + | Описание | Отдам даром за 500 рублей | + And нажимает "Опубликовать" + Then система проверяет цену на положительное значение + And система возвращает ошибку с кодом "INVALID_PRICE" + And сообщение содержит "Цена должна быть положительным числом" + And объявление НЕ создаётся + And поле "Цена" подсвечено красным + And продавец видит подсказку: + """ + Цена не может быть отрицательной. Укажите корректную сумму. + """ + + Scenario: Ошибка - заголовок слишком короткий + When продавец создаёт объявление со следующими данными: + | Поле | Значение | + | Категория | Электроника | + | Заголовок | Телефон | + | Цена | 45000 | + | Описание | Продаю iPhone 13 | + And нажимает "Опубликовать" + Then система проверяет длину заголовка (минимум 10 символов) + And система возвращает ошибку с кодом "TITLE_TOO_SHORT" + And сообщение содержит "Заголовок должен содержать минимум 10 символов" + And объявление НЕ создаётся + And поле "Заголовок" подсвечено красным + + Scenario: Ошибка - категория не выбрана (обязательное поле) + When продавец создаёт объявление со следующими данными: + | Поле | Значение | + | Категория | | + | Заголовок | iPhone 13 128GB б/у | + | Цена | 45000 | + | Описание | Отличный телефон | + And нажимает "Опубликовать" + Then система проверяет обязательные поля + And система обнаруживает, что поле "Категория" не заполнено + And система возвращает ошибку с кодом "REQUIRED_FIELD_MISSING" + And сообщение содержит "Выберите категорию товара" + And объявление НЕ создаётся + And поле "Категория" подсвечено красным + + Scenario: Ошибка - не загружено ни одного фото + When продавец создаёт объявление с заполненными полями + But не загружает ни одного фото + And нажимает "Опубликовать" + Then система проверяет наличие фотографий + And система возвращает ошибку с кодом "IMAGES_REQUIRED" + And сообщение содержит "Загрузите хотя бы одно фото товара" + And объявление НЕ создаётся + And блок загрузки фото подсвечен красным + + # ======================================== + # СЦЕНАРИИ С ОШИБКАМИ БИЗНЕС-ЛОГИКИ + # ======================================== + + Scenario: Ошибка - дубликат объявления (тот же товар за 10 минут) + Given у продавца уже есть активное объявление "iPhone 13 128GB" созданное 5 минут назад + When продавец создаёт новое объявление с таким же заголовком "iPhone 13 128GB" + And нажимает "Опубликовать" + Then система проверяет наличие дубликатов за последние 10 минут + And система обнаруживает похожее объявление "A-2026-0141" + And система возвращает ошибку с кодом "DUPLICATE_AD" + And сообщение содержит: + """ + Похожее объявление уже создано 5 минут назад. Пожалуйста, проверьте статус. + """ + And новое объявление НЕ создаётся + And продавец видит ссылку на существующее объявление + + Scenario: Ошибка - пользователь не верифицирован + Given пользователь "unverified@example.com" не подтвердил email + When пользователь пытается создать объявление + And нажимает "Опубликовать" + Then система проверяет статус верификации пользователя + And система возвращает ошибку с кодом "USER_NOT_VERIFIED" + And сообщение содержит: + """ + Для публикации объявления необходимо подтвердить email. Перейдите по ссылке в письме. + """ + And объявление НЕ создаётся + + Scenario: Ошибка - объявление не найдено при редактировании + Given объявления с ID "A-999999" не существует в системе + When продавец пытается отредактировать объявление "A-999999" + Then система проверяет существование объявления + And система возвращает ошибку с кодом "AD_NOT_FOUND" + And сообщение содержит "Объявление с ID A-999999 не найдено" + And HTTP ответ имеет статус код 404 (Not Found) + + # ======================================== + # СЦЕНАРИИ СБОЕВ ИНФРАСТРУКТУРЫ + # ======================================== + + Scenario: Таймаут ML-модератора (Anti-spam API недоступен) + Given "ML-модератор" недоступен (таймаут 3 секунды) + When продавец создаёт объявление со всеми данными и загружает фото + And нажимает "Опубликовать" + Then система загружает фото в хранилище и получает URL + And система сохраняет объявление в БД + + When система пытается проверить текст через ML-модератора + Then система получает "TimeoutException" (сервер не отвечает 3 секунды) + And система НЕ откатывает создание объявления + And система сохраняет объявление со статусом "PENDING_MODERATION" + And система создаёт запись в очереди модерации с причиной "ML_TIMEOUT" + And система публикует событие "AdPendingModeration" с retry_count=0 + + And продавец видит предупреждение: + """ + Объявление отправлено на проверку. Обычно это занимает 5 минут. Вы получите уведомление. + """ + And HTTP ответ имеет статус код 202 (Accepted) + + And система ставит задачу в очередь на повторную проверку через 30 секунд + And в базе данных есть запись в таблице "moderation_queue": + | ad_id | reason | retry_count | next_retry_at | + | A-2026-0143 | ML_TIMEOUT | 0 | [текущее время + 30с] | + + Scenario: Таймаут хранилища изображений (Image Store недоступен) + Given "Хранилище изображений" недоступно (таймаут 5 секунд) + When продавец создаёт объявление с 3 фотографиями + And нажимает "Опубликовать" + Then система пытается загрузить фото в хранилище + And система получает "TimeoutException" (хранилище не отвечает 5 секунд) + And система откатывает всю транзакцию + And объявление НЕ создаётся + And событие "AdCreated" НЕ публикуется + + And система логирует ошибку с уровнем "ERROR": + """ + Failed to upload images to S3: Connection timeout + """ + And продавец видит сообщение: + """ + Не удалось загрузить фото. Попробуйте позже или выберите другие изображения. + """ + And HTTP ответ имеет статус код 503 (Service Unavailable) + And фронтенд сохраняет фото в LocalStorage для повторной попытки + + Scenario: База данных недоступна при сохранении объявления + Given "База данных" недоступна (connection refused) + When продавец создаёт объявление со всеми данными + And нажимает "Опубликовать" + Then система пытается сохранить объявление в БД + And система получает "DatabaseConnectionException" + And система откатывает всю транзакцию + And объявление НЕ создаётся + And фото НЕ загружаются (откат) + + And система логирует ошибку с уровнем "ERROR": + """ + Failed to create ad: Database connection failed + """ + And продавец видит сообщение: + """ + Не удалось создать объявление. Попробуйте ещё раз или свяжитесь с техподдержкой. + """ + And HTTP ответ имеет статус код 503 (Service Unavailable) + And все введённые данные остаются в форме (не сбрасываются) + + # ======================================== + # ИДЕМПОТЕНТНОСТЬ И КОНКУРЕНТНОСТЬ + # ======================================== + + Scenario: Повторное нажатие кнопки "Опубликовать" не создаёт дубликат + Given продавец заполнил все поля и загрузил фото + When продавец нажимает кнопку "Опубликовать" + And из-за медленного интернета нажимает кнопку второй раз (double-click) + Then система создаёт только ОДНО объявление + And второй запрос возвращает тот же ad_id что и первый + And система проверяет наличие дубликата по idempotency_key + And в базе данных присутствует только 1 запись для этого объявления + And продавец видит сообщение: + """ + Объявление уже создано. ID: A-2026-0142 + """ + + Scenario: Конкурентная публикация одного товара двумя продавцами + Given два продавца одновременно пытаются опубликовать объявление с одинаковым заголовком "iPhone 13" + When Продавец_1 нажимает "Опубликовать" + And Продавец_2 нажимает "Опубликовать" (одновременно) + Then система обрабатывает оба запроса независимо + And создаются два разных объявления с ID "A-2026-0142" и "A-2026-0143" + And оба объявления успешно публикуются + And система проверяет только дубликаты от ОДНОГО пользователя (не блокирует разных) + And оба продавца видят сообщение об успешной публикации + + # ======================================== + # АЛЬТЕРНАТИВНЫЕ ПОТОКИ + # ======================================== + + Scenario: Сохранение черновика объявления + When продавец начинает создавать объявление + And заполняет только: + | Поле | Значение | + | Категория | Электроника | + | Заголовок | iPhone 13 для обсуждения| + And нажимает кнопку "Сохранить в черновики" + Then система сохраняет черновик объявления со статусом "DRAFT" + And система возвращает сообщение: + """ + Черновик сохранён. Вы можете вернуться к редактированию позже. + """ + And черновик отображается в разделе "Черновики" + And объявление НЕ проходит модерацию + And другие пользователи НЕ видят его в поиске + + When продавец через час открывает черновик + And заполняет поле "Цена: 45000" + And загружает 2 фотографии + And нажимает "Опубликовать" + Then система создаёт полноценное объявление со статусом "ACTIVE" (после проверки) + And черновик удаляется из раздела черновиков + + Scenario: Платное поднятие объявления (буст в топ) + Given продавец создал объявление и оно успешно опубликовано + When продавец открывает карточку объявления + And выбирает опцию "Поднять в топ на 3 дня" + Then система отображает стоимость услуги: "199 рублей" + + When продавец подтверждает оплату + Then система создаёт платёжную ссылку + And после успешной оплаты обновляет объявление: + | Поле | Значение | + | boost_expires_at | NOW() + 3 days | + | priority | HIGH | + And объявление получает приоритет при сортировке + And продавец видит сообщение: + """ + Ваше объявление поднято в топ до 15.03.2026 + """ + + # ======================================== + # ГРАНИЧНЫЕ СЛУЧАИ + # ======================================== + + Scenario: Создание объявления с минимальной ценой + When продавец создаёт объявление с ценой "1.00" RUB + Then система принимает объявление + And объявление создаётся успешно со статусом "ACTIVE" + And объявление отображается в поиске + + Scenario: Создание объявления с максимальной длиной описания + Given максимальная длина описания составляет 5000 символов + When продавец создаёт объявление с описанием ровно на 5000 символов + Then система принимает объявление + And объявление создаётся успешно + + Scenario: Отклонение объявления модератором (после ручной проверки) + Given продавец опубликовал объявление, и ML-модератор отправил его в "PENDING_MODERATION" + When живой модератор проверяет объявление и обнаруживает запрещённый товар (оружие) + Then модератор нажимает "Отклонить" и указывает причину: "Запрещённая категория товаров" + Then система обновляет статус объявления на "REJECTED" + And система отправляет email продавцу с причиной отклонения + And продавец видит в личном кабинете статус "Отклонено: Запрещённая категория товаров" + And продавец может отредактировать объявление и отправить на повторную проверку \ No newline at end of file diff --git a/students/Tsuytskou_Kiryl/lab_01/use-case.md b/students/Tsuytskou_Kiryl/lab_01/use-case.md new file mode 100644 index 00000000..a160996d --- /dev/null +++ b/students/Tsuytskou_Kiryl/lab_01/use-case.md @@ -0,0 +1,132 @@ +# Use-case: Публикация объявления «Бери, пока горячее» + +**Предметная область:** Доска объявлений «Бери, пока горячее» + +**Первичный актор:** Пользователь (Продавец) + +**Вторичные акторы:** +- Модератор (внешняя система/человек) +- Система модерации (анти-спам / ML) +- Сервис уведомлений (Email/Push) +- Image Storage (S3/MinIO) + +**Цель:** Быстро разместить объявление о продаже товара так, чтобы оно прошло базовую проверку и стало доступно другим пользователям. + +--- + +## Предусловия + +- Пользователь авторизован в системе. +- Пользователь подтвердил email или телефон. +- Категория товара существует в системе. + +--- + +## Основной поток (Happy Path) + +1. Пользователь нажимает кнопку «Подать объявление». +2. Система отображает форму с полями: Категория, Заголовок, Описание, Цена, Фото (до 5 шт.). +3. Пользователь заполняет форму и нажимает «Опубликовать». +4. Система проверяет обязательные поля (заголовок, цена, категория, фото). +5. Система отправляет фото в Image Storage и получает URL. +6. Система запускает пре-модерацию (базовый спам-фильтр по тексту). +7. Система не находит запрещённых слов и признаков мошенничества. +8. Система сохраняет объявление в БД со статусом «Активно». +9. Система отправляет асинхронное уведомление (Push/Email): «Ваше объявление опубликовано». +10. Система возвращает пользователю ссылку на просмотр объявления. +11. Пользователь видит статус «Активно. Ждём покупателей!». + +--- + +## Постусловия + +- В БД создана запись объявления со статусом ACTIVE. +- Загруженные изображения сохранены в облачном хранилище. +- Пользователь получил подтверждение. + +--- + +## Альтернативные потоки + +### 3a. Пользователь сохраняет черновик + +1. 3a1. Пользователь заполняет только заголовок и категорию. +2. 3a2. Нажимает «Сохранить в черновики». +3. 3a3. Система создаёт объявление со статусом DRAFT. +4. 3a4. Сценарий завершается (объявление не видно другим). + +### 3b. Пользователь поднимает объявление (платное) + +1. 3b1. Пользователь выбирает опцию «Поднять в топ на 3 дня». +2. 3b2. Система запрашивает оплату. +3. 3b3. После оплаты поле boost_expires_at устанавливается на NOW() + 3 days. +4. 3b4. Объявление получает приоритет при сортировке. + +--- + +## Исключительные ситуации + +### 5a. Модерация не пройдена (Спам или фейк) + +**Условие:** ML-модератор обнаружил мат, рекламу или обман. + +**Поток:** +1. 5a1. Система сохраняет объявление, но со статусом «На модерации». +2. 5a2. Отправляет задачу живому модератору. +3. 5a3. Пользователь видит "Объявление отправлено на проверку. Обычно это занимает 5 минут". +4. 5a4. При отклонении → статус REJECTED, пользователь получает причину. + +### 8a. Ошибка загрузки фото + +**Условие:** Image Storage недоступен. + +**Поток:** +1. 8a1. Система откатывает транзакцию (объявление не создаётся). +2. 8a2. Пользователь получает ошибку: "Не удалось загрузить фото. Попробуйте позже". +3. 8a3. Фронтенд сохраняет файлы локально для retry. + +### 8b. Ошибка Anti-spam API + +**Условие:** Внешний сервис модерации вернул 500 ошибку или таймаут. + +**Поток:** +1. 8b1. Сервис модерации недоступен. +2. 8b2. НЕ откат. Объявление идёт в статусе PENDING_MODERATION. +3. 8b3. Ставится задача в очередь. Повтор через 5 мин. +4. 8b4. Пользователь видит: "Проверка антисписком задерживается. Объявление появится автоматически". + +### 9a. Дубликат объявления + +**Условие:** Пользователь пытается продать точно такой же товар (тот же заголовок + фото). + +**Поток:** +1. 9a1. Система проверяет: было ли объявление от этого пользователя с таким хешем текста за последние 10 минут. +2. 9a2. Отвечает: 409 Conflict. "Похожее объявление уже создано. Пожалуйста, проверьте статус". + +--- + +## Частота использования + +- **Высокая** - в среднем 500-1000 новых объявлений в день (активные пользователи). +- **Пиковая нагрузка** - до 5000-7000 объявлений в день в выходные и перед праздниками (люди разбирают вещи, готовятся к сезону). + +## Критические требования + +- **Производительность:** Создание < 1с, загрузка фото < 3с, поиск < 500мс, карточка < 200мс +- **Доступность:** 99.5% (рабочее время), поиск — 99.9%, ночной downtime — до 4ч +- **Идемпотентность:** Двойной клик не создаёт дубликаты, защита от повторной отправки +- **Безопасность:** Только авторизованные + подтверждённый email/телефон + XSS-защита + запрещённые категории +- **Аудит:** Логи модераторов и изменений цены/статуса (хранение ≥ 90 дней) +- **Целостность данных:** Атомарность цены, после удаления — 410 Gone, история изменений для споров + +--- + +## Открытые вопросы + +1. Авто-обновление статуса: Нужно ли автоматически переводить объявление из "Активно" в "Продано", когда пользователь нажал "Отметить как проданное"? Или оставить ручное управление? +2. Буст объявлений (платное поднятие): Поддерживаем ли мы внутреннюю валюту (монеты/баллы) или используем реальные деньги через платёжный шлюз? Если реальные деньги — как обрабатывать возврат за неоказанную услугу? +3. История цен: Должен ли покупатель видеть, что продавец снижал цену за последнюю неделю (например, "было 5000₽, стало 4000₽")? Это увеличивает доверие, но усложняет модель данных. +4. Геолокация: Нужен ли фильтр по расстоянию (в радиусе 5 км) или достаточно выбора города/района? Требует ли это интеграции с картами и расчёта стоимости доставки? +5. Чат между продавцом и покупателем: Должен ли он работать через WebSocket (реал-тайм) или достаточно опроса (polling) раз в 5 секунд? Как модерировать сообщения на предмет спама? +6. Повторная модерация: Если объявление уже было отклонено модератором, может ли пользователь исправить его и отправить на повторную проверку? Или оно блокируется навсегда? +7. Жалобы на объявление: Что происходит, когда 5 разных пользователей пожаловались на одно объявление? Автоматически скрывать? Отправлять модератору? Как защитить от накрутки жалоб конкурентами?