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

Генератор отчётов

Фоновое формирование отчётов с сохраняемым состоянием задания, отменой и восстановлением после перезапуска.

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

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

Создайте на Go REST API асинхронного отчёта expenses_by_category с PostgreSQL, миграциями и SQL без ORM. Таблица report_records содержит id, occurred_at, category и amount_minor_rub; amount_minor_rub — целое от 1 до 1,000,000,000,000 копеек. POST /api/report-records принимает occurredAt в RFC 3339 UTC, category длиной 1–80 символов и amountMinor от 1 до 1,000,000,000,000 копеек, отвечает 201; записи неизменяемы. GET /api/report-records имеет limit 1–100 и cursor. Job содержит id, type=expenses_by_category, from, to, snapshot_row_count, state (pending, running, completed, failed, cancelled), progress 0–100, attempt, created_at, updated_at, started_at, finished_at, error_code и JSON result. Диапазон from включителен, to исключителен; параметры сохраняются для восстановления. Долгие операции не выполняются внутри HTTP-запроса.

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

POST /api/reports принимает from и to в RFC 3339 UTC с from<to. Одна транзакция создаёт job и копирует все подходящие на момент транзакции неизменяемые записи в report_job_records(job_id, occurred_at, category, amount_minor_rub); до commit ответ 202 не отправляется. Так результат всегда строится по snapshot и не меняется от новых записей. Отчёт агрегирует сумму amount_minor_rub и число записей по category, строки сортируются по category. JSON-результат содержит totals.amountMinor и totals.recordCount, rows из category/amountMinor/recordCount, currency="RUB" и schemaVersion=1. Ошибка параметров — 400 без задания. GET /api/reports/{id} возвращает состояние, прогресс от 0 до 100, попытку и безопасный error_code; неизвестный id — 404. GET /api/reports/{id}/result доступен только при completed: для других состояний отвечает 409 not_ready, при неизвестном задании — 404. Ответ результата имеет документированный JSON-медиатип и версию схемы. POST /api/reports/{id}/cancel фиксирует запрос отмены; для pending отмена немедленная, для running исполнитель обязан регулярно проверять cancellation и прекратить работу. Завершённое, failed или уже cancelled задание возвращает 409 invalid_state.

Задания и результаты хранятся в PostgreSQL и переживают перезапуск. Число рабочих процессов настраивается от 1 до 8, по умолчанию 2; атомарный захват не допускает двойного запуска. Максимум 3 попытки: между первой и второй выдерживается 10 секунд, между второй и третьей — 30 секунд; после третьей неудачи состояние failed и задан безопасный errorCode. Время одной попытки ограничено 60 секундами. progress целое от 0 до 100. После рестарта pending и прерванные running восстанавливаются, результат completed не удаляется. Повторяются только временные ошибки; внутренние подробности не возвращаются. Отмена request context клиента не отменяет уже принятое задание; отдельный cancel endpoint управляет его жизненным циклом. Остановка сервера отменяет контексты исполнителей и оставляет запущенную работу для восстановления после перезапуска. API использует пагинацию limit/cursor при списке заданий пользователя с детерминированной сортировкой. Контексты SQL и HTTP имеют конечные таймауты; блокировки и обновление состояния соблюдают отмену. Ошибки не раскрывают SQL, секреты не логируются, индексы покрывают state/created_at, N+1 исключён.

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

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

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

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

Поставка

  • Запускаемый Go REST API с PostgreSQL-миграциями создаёт записи расходов и фоновые отчёты expenses_by_category со snapshot данных.
  • README описывает from/to, агрегаты, формат результата, пределы числа фоновых исполнителей и повторы; тестовые данные и временная база обеспечивают офлайн-тесты.

Приёмка

  • POST отчёта возвращает 202 и атомарно сохраняет snapshot; записи, добавленные после commit этой транзакции, не входят в rows.
  • Completed JSON содержит totals, rows по category в алфавитном порядке, currency=RUB и schemaVersion=1.
  • Временный сбой повторяется после пауз 10 и 30 секунд, всего выполняется не более трёх попыток; после лимита state=failed.
  • Одновременно работают не более двух исполнителей по умолчанию; конфигурация принимает 1–8, progress всегда целое 0–100.
  • Результат переживает перезапуск; cancel completed и отмена после завершения возвращают 409, время выполнения попытки ограничено 60 секундами.
  • README показывает зависимости; агрегация и переходы задания тестируются без HTTP/БД, SQL-сценарии снимка и восстановления — отдельно.