Модель данных и назначение
Реализуйте асинхронный CSV-импорт на Go с PostgreSQL, миграциями и сырым SQL без ORM. POST /api/imports принимает multipart/form-data с одним файлом размером не более 50 MiB; превышение обнаруживается потоково и возвращает 413 до принятия job. Поддерживается CSV UTF-8 с обязательными колонками source_id,name,email,amount_minor; порядок колонок не имеет значения, повтор заголовка или отсутствие/неизвестная колонка дают 400 до создания задания. Импортируемая запись содержит уникальный source_id, name, нормализованный email и целое amount_minor от 1 до 1,000,000,000,000 копеек. Строки с пустыми обязательными значениями, некорректным integer или повтором source_id внутри файла регистрируются как row errors с номером строки, кодом и сообщением без полного содержимого строки.
API, проверки и эксплуатация
Документированная политика — частичный commit по строкам: валидные строки записываются, невалидные пропускаются. Содержимое исходного файла сохраняется в PostgreSQL до ответа 202. Все успешные строки одной job применяются в одной транзакции после разбора; при инфраструктурной ошибке транзакция откатывается, задание остаётся доступным для восстановления после перезапуска и может быть повторено. source_id длиной 1–128 символов уникален во всей таблице и исключает дубли. Повторная обработка той же job не дублирует записи; новый файл с тем же source_id возвращает строку как skipped_duplicate, не заменяя ранее импортированную запись. Job содержит id, state pending/running/completed/failed/cancelled, progress целым числом 0–100, counters total/created/skipped/error, номер попытки, timestamps и ссылку на результат ошибок.
POST отвечает 202 с id, GET /api/imports/{id} отдаёт состояние и счётчики; GET /api/imports/{id}/errors возвращает строки ошибок с limit/cursor и сортировкой по row_number. POST /api/imports/{id}/cancel прекращает чтение между строками. До фиксации транзакции все изменения откатываются и job становится cancelled без внесённых строк; если commit уже выполнен, job остаётся completed, а отмена возвращает 409. Таким образом cancelled никогда не содержит применённые строки. После рестарта pending и прерванные running jobs подхватываются из БД, захват очереди атомарен, число исполнителей настраивается от 1 до 8, по умолчанию 2. Максимум 3 попытки: паузы 10 и 30 секунд; каждая попытка длится не более 300 секунд. Прогресс — целое 0–100. Временные сбои повторяются, ошибки данных нет; после третьей ошибки job становится failed. Результат ошибок и завершённое состояние сохраняются при рестарте. Размер CSV и длина полей имеют пределы, заголовки и кавычки разбираются стандартным CSV-парсером. Ошибки API имеют единый формат и статусы 400/404/409/413. Контексты SQL имеют тайм-аут и отмену. Логи не содержат строк CSV, email или имя; данные не выводятся в сообщения ошибок. Индексы обеспечивают уникальность и просмотр заданий/ошибок без N+1.
Структура программы
Код разделён на небольшие internal-пакеты: internal/httpapi принимает файл и формирует DTO, internal/imports проверяет CSV и частичную политику, internal/jobs хранит состояние, internal/workers читает файл и выполняет транзакцию. main.go связывает сервер и исполнителей; отмена передаётся через ctx, функции не вызывают os.Exit.