Repository navigation
Doc How Tour Publishing
Менеджер собирает тур в мастере из 7 шагов. Пока тур черновик, его видно только владельцу и админу. Публикация проверяет, что тур заполнен, делает его фото публичными и открывает тур в каталоге.
POST /api/v1/catalog/manage/tours
→ 201 ETag: "1" { "id": "3f2a…", "status": "Draft", … }
PUT /api/v1/catalog/manage/tours/3f2a…/basics
If-Match: "1"
{ "title": "Неделя в Стамбуле", "country": "Турция", "durationDays": 7, … }
→ 200 ETag: "2"
POST /api/v1/catalog/manage/tours/3f2a…/publish
If-Match: "2"
→ 422 { "type": "https://travelagency/errors/tour-not-publishable",
"missing": ["description", "program.dayCount", "images.minCount", …] }Менеджер дозаполняет шаги, каждый раз отправляя последнюю версию, и следующий publish проходит: тур появляется в GET /api/v1/catalog/tours.
Фронтенд TourWizardPage (/manager/tours/new, /manager/tours/:tourId), компонент TourWizard:
| № | Шаг интерфейса | Запрос |
|---|---|---|
| 1 | Основное |
PUT …/basics и PUT …/description
|
| 2 | Программа | PUT …/program |
| 3 | Включено и не включено | PUT …/conditions |
| 4 | Проживание и питание |
PUT …/conditions (шаги 3 и 4 сохраняются одним запросом) |
| 5 | Даты и цены | PUT …/prices |
| 6 | Фото | загрузка в Media, затем PUT …/images
|
| 7 | Проверка и публикация | POST …/publish |
Номер шага хранится в ?step= адресной строки.
Задача: два окна одного менеджера (или менеджер и админ) не должны молча затирать изменения друг друга.
- Каждый ответ с туром несёт
ETag: "<Version>". - Любая запись, кроме создания черновика, требует
If-Matchс этой версией (W/и кавычки допускаются). - Версия не совпала: 409. Заголовка нет: 428.
- Любое изменение агрегата, даже только фото или дней программы, вызывает доменный
Touch()и увеличиваетVersion. EF Core используетVersionкак токен конкуренции: если между чтением и сохранением версия сменилась,UPDATE … WHERE Version = <старая>не найдёт строку, и ответ тоже 409.
Почему не системный столбец PostgreSQL xmin: он меняется, только когда переписывается строка Tours. Шаг, который меняет одни фото, строку тура не трогает, и устаревший If-Match прошёл бы проверку.
Коды лежат в TourPublishRequirementCodes (Catalog.Domain), это единственный источник. Тексты для пользователя только на фронтенде (src/domain/tourManageErrors.ts, тест проверяет, что у каждого кода есть текст).
| Код | Условие |
|---|---|
title, shortDescription, description
|
тексты заполнены |
program.dayCount |
дней в программе ровно DurationDays
|
inclusions.included |
хотя бы один пункт «включено» |
mealPlan, accommodation
|
питание и проживание заданы |
offers.future |
хотя бы одно предложение с будущей датой |
images.cover |
есть обложка |
images.minCount |
не меньше 3 фото (максимум 20) |
images.coverMinWidth |
ширина оригинала обложки не меньше 1280 px; ширина 0 (неизвестна) не проходит |
Опубликованный тур проверяется и при правке: изменение, которое ломает проверку, получает 422, а тур сам с публикации не снимается.
- Загрузить тур, сверить
If-Match. - Через gRPC
GetMediaFilesполучить у Media все фото тура: каждое должно принадлежать владельцу тура; взять ширину обложки. - Проверить требования. Есть пропуски: 422 с
missing. - Вызвать
MarkMediaFilesPublicдля всех фото тура. Media недоступен: 503, статус тура не меняется, повтор безопасен. - Только после успешного вызова поставить
PublishedиPublishedAt, сохранить.
Порядок «сначала фото, потом статус» гарантирует, что в момент публикации все фото тура уже открыты.
PUT images тоже проверяет через GetMediaFiles владельца каждого фото, назначение tour-image и ширину обложки. Чужой файл, неизвестный id и другое назначение дают один 422 images.notFound. Обложка меньше 1280 px: 422 сразу при сохранении. Если тур уже Published, перед заменой фото и сохранением вызывается MarkMediaFilesPublic для всех id из запроса. Media недоступен: 503, список фото тура не меняется, повтор безопасен. В мастере при этом остаётся текст «Не удалось обновить фото тура. Изменения не сохранены, попробуйте ещё раз через минуту», выбранные фото не сбрасываются. У черновика и снятого тура флаг не ставится: его поставит следующий publish.
-
unpublish:Published→Unpublished. Тур пропадает из каталога и со страницы (404), в избранном остаётся карточкой сavailable: false. Фото остаются публичными, чтобы карточка избранного не сломалась. -
DELETE: только черновик, иначе 409.
| Manager | Admin | |
|---|---|---|
| список | свои туры | все |
| создать черновик | да, становится владельцем | да |
| править, публиковать, снимать, удалять | только свои (иначе 403) | любые |
Предварительный просмотр фото черновика идёт через /api/v1/media/manage/files/{id}/{size}, публичные адреса до публикации отвечают 404. См. Картинки.
Система
API
Сервисы
Как реализовано
Справочное
- Видение продукта
- Дорожная карта
- Этап 1: что осталось
- Этап 2: расширение
- Этап 3: масштабирование
- Концепция сайта и интерфейса
- Шаблон ТЗ