Модель данных и назначение
Создайте 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 не используется.