BR 6.1 → Store Service
Репозиторий: erp-store-service
Контракты: API, Data Model, Events
Задачи
Инфраструктура / миграции
- Liquibase changeset
NNN-marketing-slides.xml:- Создаёт таблицу
marketing_slides(по Data Model) - Все индексы (
idx_marketing_slides_store,idx_marketing_slides_store_active_order,uq_marketing_slides_store_order) - CHECK-constraints:
image_mime,source,order_index >= 0,image_size_bytes 1..10485760
- Создаёт таблицу
- Liquibase changeset
NNN-stores-standby-config.xml:- ALTER TABLE
storesADDstandby_idle_minutesinteger NOT NULL DEFAULT 5 - ALTER TABLE
storesADDstandby_transition_secondsinteger NOT NULL DEFAULT 9
- ALTER TABLE
- Liquibase changeset для outbox-таблицы маркетинга (или общая outbox-таблица — посмотреть как сделано в текущем
store.table.upserted— там fire-and-forget без outbox; здесь нужен полноценный outbox)- Таблица
marketing_outbox(id, event_type, payload jsonb, created_at, processed_at) - Composite index
(processed_at, created_at)для poller’а
- Таблица
Entity / Repository (JPA)
-
MarketingSlideentity (поля по Data Model) -
MarketingSlideRepositoryextends JpaRepository -
MarketingOutboxentity + repository
S3 интеграция
- Внести
erp-marketing-slidesбакет в конфиг (application.yml) - Сервис
MarketingSlideStorageServiceс методами:upload(storeId, slideId, MultipartFile) → publicUrldelete(objectKey)(для отложенной очистки)
- Использовать существующий S3Client (если есть для фото ТТ) или поднять новый
- Public URL должен быть стабильный (не presigned) —
https://s3.erp/erp-marketing-slides/{store_id}/{slide_id}.{ext}через bucket policy (read-only public)
Бизнес-логика
- Сервис
MarketingSlideService:list(storeId, activeFilter?) → List<DTO>listActive(storeId) → ActiveBundleDTO(для internal: slides + standby_config из stores)create(storeId, file, title, active) → DTO- Валидация MIME/size/dimensions (≥ 1280×720)
- Проверка лимита 20 активных
- Upload в S3
- INSERT в
marketing_slidesсorder_index = MAX + 1 - INSERT в
marketing_outbox(action=created) - Один транзакционный блок (если outbox в той же БД, что и slides — это просто
@Transactional)
update(storeId, slideId, file?, title?, active?) → DTO- Если file — старый file_url пишем в
marketing_outboxдля отложенной очистки (или в отдельную таблицу cleanup queue) - INSERT в outbox (action=updated|activated|deactivated)
- Если file — старый file_url пишем в
reorder(storeId, List<{id, order}>) → List<DTO>- Транзакционно — снимаем
uq_marketing_slides_store_orderчерез temp-shift (UPDATE order_index = order_index + 1000 WHERE store_id = ?), затем выставляем правильные - INSERT в outbox (action=reordered, slide_id=NULL)
- Транзакционно — снимаем
delete(storeId, slideId)— soft delete + outbox (action=deleted)
- Ролевые проверки в
MarketingSlideController:marketing.readдля GETmarketing.writeдля POST/PUT/PATCH/DELETE- Scope-check: store_id из path должен быть в scope пользователя (правила ролевой модели)
HTTP endpoints
-
MarketingSlideController:GET /api/v1/admin/stores/{storeId}/marketing-slidesPOST /api/v1/admin/stores/{storeId}/marketing-slides(multipart)PUT /api/v1/admin/stores/{storeId}/marketing-slides/{slideId}(multipart или json)PATCH /api/v1/admin/stores/{storeId}/marketing-slides/reorderDELETE /api/v1/admin/stores/{storeId}/marketing-slides/{slideId}
-
InternalMarketingController:GET /internal/stores/{storeId}/marketing-slides/active— slides + standby конфиг
Kafka publisher (outbox poller)
- Scheduled task (каждые 1 сек или event-driven):
- SELECT FROM
marketing_outboxWHEREprocessed_at IS NULLORDER BY created_at LIMIT 100 - Для каждой записи — KafkaTemplate.send(“marketing.slide.changed”, store_id, payload)
- На success — UPDATE processed_at = now()
- На failure — лог + следующий заход (retry бесконечно, идемпотентность через consumer-side dedup по event_id)
- SELECT FROM
- Конфиг Kafka топика
marketing.slide.changed(черезkafka-topics-initконтейнер в инфре)
Расширения существующих эндпоинтов
-
PATCH /api/v1/stores/{id}— разрешитьstandby_idle_minutesиstandby_transition_secondsв body (только для роли Franchise / scope-owner)- Валидация: 1 ≤ idle_minutes ≤ 60, 3 ≤ transition_seconds ≤ 60
Cron / очистка S3
- Scheduled task раз в сутки (например, 03:00 UTC):
- SELECT image_url FROM
marketing_slidesWHEREdeleted_at IS NULL - Сравнить с listObjects бакета
erp-marketing-slides - Удалить объекты без ссылки
- Сохранять deleted_at в последние 7 дней (грация для отката)
- SELECT image_url FROM
Тестирование
- Юнит-тесты
MarketingSlideService:- CRUD happy path
- Reorder в транзакции
- Лимит 20 активных
- Валидация MIME/size
- Soft delete
- Интеграционный тест: outbox-publisher публикует в Kafka
- Юнит-тест ролевых проверок в Controller
Seed (для test VPS)
- См. Seed — отдельная задача
Зависимости
- User Service миграция permissions — должна задеплоиться до этого сервиса (RBAC-чек)
- Бакет
erp-marketing-slidesна MinIO — создать ДО первого деплоя сервиса - Kafka topic
marketing.slide.changed— добавить вkafka-topics-init
Готово когда
- Все 5 публичных + 1 internal эндпоинты проходят smoke-тесты на test VPS
- Создание слайда публикует Kafka-событие, pos-bff его получает (видно в логах)
- Лимит 20 активных слайдов работает
- Soft delete работает + cron очистки S3 настроен (можно отдельно проверить вручную)