← назад в блог
daily6 августа 2026 г./5 мин чтения

Функция загрузки файлов, которую я реализовал, удалив большую часть собственного дизайна

Мы добавили шаг «Причина отмены» в мастер онлайн-расторжения страхового полиса: быстрый выбор причины, произвольный комментарий и необязательное прикрепление PDF-документа. Моё первое решение включало два новых API, отдельную таблицу в базе данных, машину состояний и фоновую очистку неиспользуемых файлов. В финальной версии не осталось ничего из этого: файл отправляется вместе с уже существующим запросом, хранится в S3, а база сохраняет лишь ссылку на него. Эта история о том, как пересмотр собственного решения сделал функцию проще, безопаснее и позволил выпустить её быстрее.

Снимок экрана — 2026-08-07 в 08.33.27.png

Когда мы добавили в мастер онлайн-расторжения страхового полиса шаг «Причина отмены» — почему вы расторгаете договор, своими словами, плюс необязательный PDF — моя первая архитектура загрузки файла включала два новых API, новую таблицу в базе данных, машину состояний для временных файлов и фоновую задачу очистки «осиротевших» документов. В продакшн не попало ничего из этого. Это история о том, как пересмотр собственного решения сделал функцию меньше, безопаснее и позволил выпустить её быстрее.

Сам шаг: варианты, которые печатают за вас

С точки зрения пользователя шаг выглядит просто: три быстрых варианта («Продажа автомобиля», «Переход в другую страховую компанию», «Другое»), обычное текстовое поле и прикрепление документа. В первой версии варианты работали как скрытые метки — пользователь нажимал кнопку, она подсвечивалась, а её текст незаметно добавлялся к запросу только при отправке формы.

Замечание коллеги изменило моё мнение: «Если этот текст в итоге попадёт в заявку, почему пользователь его вообще не видит?» Теперь при выборе варианта его текст сразу появляется в поле комментария, где его можно изменить, удалить или дописать собственное объяснение. Если вариант снять, удаляется ровно та фраза, которую вставила система, — и только если пользователь не успел изменить её своими словами, потому что ваш текст принадлежит вам. Сотрудник компании получает ровно тот текст, который человек видит перед собой. Никаких скрытых данных, никаких невидимых параметров.

Архитектура загрузки, которую я почти реализовал

PDF прикрепляется за два шага до того, как сама заявка вообще появляется в системе. Моя первая идея выглядела очень «правильной» с инженерной точки зрения: файл сразу загружается на сервер после выбора, сервер возвращает идентификатор документа, идентификатор переносится между шагами мастера и связывается с заявкой при окончательной отправке. Но такая архитектура автоматически требовала ещё множество компонентов: отдельный API для загрузки, API для удаления файла при нажатии кнопки «×», таблицу временных файлов с состояниями pending и linked, проверку владельца, ограничения против злоупотреблений и ежедневную фоновую задачу, удаляющую забытые документы.

Именно в этот момент коллега задал вопрос, который изменил план: у нас уже есть API для расчёта, сохранения и проверки статуса — может быть, можно решить задачу прагматичнее?

Ответ дал повторный проход по реальному пути пользователя. Документ необязательный. Файл всегда только один. А существующий API сохранения уже умеет сохранять всю заявку в базе данных. Вся сложная архитектура существовала лишь ради управления серверным состоянием файла до создания заявки — но бизнес-требования вообще не нуждались в этом состоянии так рано.

Файл живёт в памяти браузера, пока действительно не понадобится

Финальное решение: после выбора PDF хранится только в памяти браузера как обычный объект File до окончательной отправки. Кнопка «×»? Просто очищается одна переменная — ноль сетевых запросов, ноль серверного мусора, никакой последующей очистки. Переходы между шагами не теряют файл, потому что весь мастер — один компонент. Когда пользователь нажимает «Отправить», уходит тот же самый запрос сохранения, который существовал раньше, только как multipart/form-data с одной дополнительной частью — файлом. Если файла нет, запрос остаётся абсолютно тем же JSON, что и раньше, байт в байт.

Один запрос на входе — три колонки на выходе. Backend проверяет PDF до обращения к внутренней страховой системе (повреждённый файл отклоняется, пока ещё ничего не создано), затем сохраняет содержимое в нашем S3-хранилище, а в ту же строку базы данных, которую он и так создавал, записывает лишь ссылку: ключ объекта, оригинальное имя файла и размер. Сама страховая система вообще не знает о существовании документа — для неё процесс остаётся прежним.

Детали, которые делают решение надёжным

Заявка на возврат никогда не должна завершаться ошибкой из-за необязательного вложения — поэтому запись в S3 выполняется по принципу best effort. Если хранилище дало сбой уже после создания заявки, пользователь всё равно получает успешный ответ, а в нём — поле document_uploaded: false, так что система не скрывает проблему, но и не блокирует оформление. Сервер проверяет, что файл действительно PDF, анализируя его реальное содержимое, а не MIME-тип, который сообщает браузер: текстовый файл, переименованный в .pdf, проверку не пройдёт. А стандартный таймаут HTTP-клиента в 15 секунд — достаточный для JSON — для multipart-запросов увеличен до 60 секунд, потому что 10 МБ через медленный мобильный интернет заслуживают терпения.

До первого реального пользователя я проверил всю связку тестами с заглушками на каждом уровне: конструктор запросов — 13 отдельных проверок (JSON остаётся JSON без файла, FormData содержит все необходимые поля плюс PDF), а реальный класс серверной валидации — на корректном PDF, текстовом файле, переименованном в .pdf, слишком большом файле и задвоенном вложении. Все тесты зелёные, и ни одной настоящей заявки на расторжение в процессе создано не было.

Что я вынес из этой задачи

Первоначальная архитектура не была неправильной — она подходит для обязательных документов, нескольких файлов и независимых повторных загрузок. Она была неправильной именно для этих требований. Урок, который я усваиваю снова и снова: когда архитектура начинает обрастать новыми API, таблицами и фоновыми задачами, стоит ещё раз пройти реальный путь пользователя и честно спросить, какую проблему решает каждый из этих элементов. Иногда самое зрелое инженерное решение — удалить большую часть собственного проекта ещё до того, как кто-то успеет его реализовать.