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