← Все проекты уровня 9
Уровень 9 · Фоновые заданияВариант C

Обновление цен

Пакетный пересчёт каталога с десятичной точностью, фиксированной областью действия и защитой от устаревшей ревизии.

Техническое задание

Модель данных и назначение

Создайте Go REST API пересчёта цен в PostgreSQL с миграциями и параметризованным SQL без ORM. Product хранит id, sku, category_id, price NUMERIC(12,2), revision, active и timestamps. POST /api/categories принимает непустое уникальное name и отвечает 201. POST /api/products принимает sku, categoryId и price как десятичную строку от 0.01 до 1,000,000,000.00, отвечает 201; GET /api/products имеет limit 1–100 и cursor. PATCH /api/products/{id} меняет sku, categoryId, price или active и отвечает 200. Создание или изменение товара атомарно увеличивает catalogueRevision. GET /api/catalogue/revision возвращает текущую целочисленную ревизию каталога. Коэффициент — десятичная строка, 0<coefficient<=10 и не более 6 знаков после точки. Результат фиксирует прежнюю и новую цену для каждого товара. Округление — HALF_UP до двух знаков.

API, проверки и эксплуатация

POST /api/recalculations принимает expectedRevision из GET /api/catalogue/revision, необязательный существующий categoryId, минимальную/максимальную цену и coefficient в заданном диапазоне. Условия фильтра комбинируются как пересечение; границы цен включительны, categoryId должен существовать. Одна транзакция проверяет expectedRevision и сохраняет подходящие товары в recalculation_items(job_id, product_id, product_revision, old_price, new_price); при несовпадении возвращается 409 revision_conflict без job. Успех — 202 с job id. Рабочее задание использует только сохранённые строки снимка. GET /api/recalculations/{id} всегда возвращает 200 для существующей job, включая state=failed и errorCode=revision_conflict, вместе со счётчиками; неизвестный id — 404. GET /api/recalculations/{id}/result доступен только после completed и использует limit 1–100/cursor с порядком product_id. POST запуска с устаревшей expectedRevision, запрос результата до completed или отмена завершённой job дают 409. POST /api/recalculations/{id}/cancel разрешён для pending/running и приводит к отмене на безопасной границе партии.

Применение обновлений атомарно по всем строкам recalculation_items: либо все цены получают одну result_revision, либо ни одна не меняется. Если revision любого продукта отличается от сохранённой, job становится failed с errorCode=revision_conflict, старые цены не перезаписываются. Повторная обработка не создаёт новую ревизию и не дублирует результат; задание атомарно захватывается одним исполнителем. Сохраняемое состояние восстанавливает pending и прерванные running после перезапуска. Число исполнителей настраивается от 1 до 8, по умолчанию 2. Максимум 3 попытки с паузами 10 и 30 секунд; время попытки не более 60 секунд, progress — целое 0–100. После перезапуска pending/running восстанавливаются, завершённый результат сохраняется. Отмена completed, failed или cancelled job возвращает 409; pending/running отменяется на границе транзакции. Контекстные SQL тайм-аут соблюдают cancellation; shutdown оставляет задание восстанавливаемым. Числовые входы проверяются до SQL, float64 не используется. Индексы покрывают snapshot/scope, состояние и продукт. API ограничивает тело, исключает N+1 и возвращает безопасные ошибки; логи не включают цены конкретного клиента или внутренние SQL-ошибки.

Структура программы

Код разделён на небольшие internal-пакеты: internal/httpapi содержит обработчики и DTO, internal/pricing задаёт точность, округление и конфликты ревизий, internal/jobs управляет состояниями, internal/workers применяет снимок каталога. main.go связывает сервер, БД и исполнителей; отмена передаётся через ctx, os.Exit не используется.

Критерии готовности

Ожидаемый результат

Поставка

  • Запускаемый Go REST API, PostgreSQL-миграции и README описывают каталог товаров, revision, decimal-округление, snapshot и ошибки заданий.
  • API предоставляет создание и пагинацию товаров, текущую catalogueRevision, запуск пересчёта, состояние и страницу результата.
  • Детерминированные офлайн-тесты используют локальную базу, фиксированные decimal-строки и управляемый тестовый исполнитель.

Приёмка

  • Цена 10.00 при коэффициенте 1.0005 становится 10.01 по правилу HALF_UP; коэффициент больше 10 или scale больше 6 получает 400.
  • GET существующей failed job возвращает 200, state=failed и errorCode=revision_conflict.
  • Изменение товара после snapshot не перезаписывает товары и не публикует частичный результат.
  • Успешная фиксация задаёт один result_revision; повторный запуск не меняет revision и результат доступен после рестарта.
  • Число исполнителей настраивается от 1 до 8, по умолчанию 2; три попытки используют паузы 10 и 30 секунд, время выполнения ограничено 60 секундами, progress — целое 0–100.
  • README показывает зависимости; округление и конфликты ревизий тестируются без HTTP/БД, атомарное применение снимка — отдельными SQL-сценариями.