Модель данных и назначение
Реализуйте REST API складского учёта на Go и PostgreSQL с миграциями и ручным параметризованным SQL, без ORM. Product содержит id, sku, name, integer stock, active и timestamps; sku уникален после trim с нечувствительным к регистру сравнением. StockMovement — неизменяемая запись с id, product_id, kind (receipt или issue), positive integer quantity, request_key, note и created_at. Текущий остаток должен согласованно отражать журнал. Количество не дробное и не может быть отрицательным; арифметика проверяет переполнение допустимого целого диапазона до записи.
API, проверки и эксплуатация
POST /api/products создаёт продукт с остатком 0 и отвечает 201; sku и name обязательны, name длиной 1–160 символов после trim, повтор SKU возвращает 409. GET /api/products принимает limit (default 50, максимум 200), offset и необязательный q для поиска по SKU или имени без учёта регистра; ответ включает items и total и сортирует по sku,id. GET /api/products/{id} возвращает продукт или 404. POST /api/products/{id}/movements принимает kind, quantity, requestKey и note. quantity от 1 до 1,000,000,000, requestKey обязателен и ограничен 128 символами. Один requestKey уникален глобально в пределах сервиса: повтор того же запроса возвращает прежний результат с признаком replay, а использование ключа с другим product/kind/quantity даёт 409 idempotency_conflict.
Изменение журнала и остатка выполняется в одной транзакции. При issue недостаточный остаток даёт 409 insufficient_stock, не добавляя движение. При конкурентных списаниях итоговый остаток никогда не становится отрицательным, а для каждой принятой операции создаётся ровно одно движение. При receipt превышение верхнего лимита возвращает 409 stock_limit. GET /api/products/{id}/movements поддерживает временной диапазон и cursor, возвращая журнал в стабильном порядке created_at,id. Все пользовательские ошибки имеют стабильные коды и статусы: 400 для формы, 404 для отсутствующей записи, 409 для конфликта состояния или уникальности. Ввод неизвестных полей и слишком большое тело отклоняется. Контекст SQL ограничен по времени и отмена запроса прекращает работу; операции с несколькими записями атомарны. Индексы обеспечивают уникальность SKU/requestKey и чтение истории без N+1. Секреты и персональные данные в логах отсутствуют; note не выводится в операционные логи.
Структура программы
Код разделён на небольшие internal-пакеты: internal/httpapi содержит обработчики и DTO, internal/stock — арифметику остатков и идемпотентность движений, internal/postgres — транзакции и SQL. main.go связывает приложение и управляет запуском. DTO не входят в домен; правила тестируются без HTTP, SQL-сценарии отдельно.