Я Javaразработчик в АльфаСтрахование, в команде Авиа Блока. Мы занимаемся страхованием авиапассажиров, и одна из наших прикладных задач — генерация страховых полисов в PDF.
Исторически печать у нас была построена на Eclipse BIRT. Если коротко, BIRT — это движок отчётности и рендеринга документов, а файл .rptdesign — его шаблон в XMLформате: в нём описаны структура документа, таблицы, параметры, стили и скрипты.
Мы внедрили подход, где итоговый документ собирается не из одной «монолитной» .rptdesign, а из базового шаблона, переиспользуемых блоков и версионированных наборов значений. Поверх BIRT появился backend-слой, который отвечает за композицию документа, каскадное версионирование, импорт/экспорт конфигурации между средами и безопасный рендер итогового PDF через BIRT runtime.
Ключевой фокус был не в ускорении рендеринга, а в user experience: изменения в структуре и контенте должны выполнять нетехнические пользователи, а продукт — контролировать качество через управляемый процесс публикации (черновики → product review → публикация версии → откат при необходимости).
В чём была реальная боль
Всё начиналось с обычной просьбы: поправить текст в полисе. Но за такой правкой быстро обнаруживалась проблема: один и тот же документ существовал в нескольких .rptdesign, у партнёров были свои вариации, а любое изменение снова шло через разработчика.
Хотим:
Исправить формулировку;
Поменять логотип партнёра;
Вынести адрес из статичного текста в параметр;
Переставить местами два смысловых блока.
Получаем:
Разработчик открывает
.rptdesign;Правит XML/таблицы/скрипты;
Проверяет, что соседние продукты не сломались;
Выкатывает новую версию;
Через пару недель повторяет всё снова, но уже для другого партнёра.
Пока документов мало — терпимо. Когда продуктов и партнёров становится много, система расползается: одни и те же куски появляются в десятках копий, версия документа перестаёт быть понятной сущностью, а бизнес оказывается зависим от IT даже в простых изменениях.
Почему одного BIRT оказалось недостаточно
BIRT хорошо решает задачу рендеринга: берёт .rptdesign, подставляет параметры и отдаёт PDF. Но BIRT сам по себе не знает о наших бизнес‑инвариантах — правилах, которые важны именно для страховых документов:
новые документы должны печататься по новой версии, а старые — по той версии, по которой были выпущены;
один и тот же логотип, текст оферты или юридическая вставка должны переиспользоваться между разными документами, но управляться независимо.
То есть движок рендеринга уже был — не хватало предметного слоя поверх него.
Правильная единица изменения
Ключевой вопрос был простой: «что именно является единицей изменения?» Не документ целиком, потому что в реальности меняются локальные части:
шапка;
таблица условий;
логотип;
юридическая вставка;
порядок блоков;
набор значений для конкретного партнёра.
После этого архитектура начала перестраиваться практически сама.
Модель: от трёх слоёв к четырём
Изначально удобно было объяснять модель тремя уровнями: макет → блок → данные блока. Но в живом проекте модель доросла до четырёх уровней.
Макет (layout)
Макет отвечает за сборку документа для связки партнёр + продукт + версия и перестаёт быть «файлом на диске», превращаясь в предметную сущность с конфигурацией:
какие наборы данных подключены;
в каком порядке выводятся блоки;
используется ли режим
onlyBlocks;какой у макета статус.
Блок (block)
Блок — переиспользуемый фрагмент документа: шапка, маршрут, таблица рисков, оферта, юридический текст, подписи. Технически блок хранит BIRT-совместимый XML-фрагмент, который можно встроить в итоговый документ.
Важно, что сам блок отвечает только за отдельный кусок документа. А вот за композицию документа целиком отвечает уже layout: именно он хранит, какие блоки участвуют в сборке и в каком порядке они идут. Для бизнеса это не «техническая деталь», а часть продуктовой логики: в зависимости от продукта, партнёра или сценария один и тот же набор блоков может идти в разной последовательности.
Данные блока (blockData)
BlockData — конкретная версия настроек блока: какой логотип подставить, какой текст показать, какие ссылки использовать, какие значения применить для конкретного партнёра/продукта. Этот слой отделяет структуру блока от наполнения.
Справочники и версии значений
Со временем выяснилось, что даже blockData разрастается, если хранить всё как свободный JSON. Поэтому появились:
DataReference— описание параметра (что это за значение).DataReferenceValue— конкретная версия значения (какое именно значение в этой версии).
В более приземлённом виде это работает так:
у блока
HEADERесть параметрlogo;DataReferenceописывает сам параметр: «это логотип партнёра, картинка, обязательное поле»;DataReferenceValueхранит уже конкретные версии значения: например,logo-v1.pngиlogo-v2.png;blockDataне дублирует эти файлы и строки внутри себя, а ссылается на нужные версии значений.
Так модель становится заметно понятнее. blockData отвечает на вопрос «что нужно этому блоку в этой конфигурации», а DataReference и DataReferenceValue — «какие именно значения доступны и какая версия сейчас выбрана».
Это даёт управляемость: система знает, где используется значение, может каскадно обновлять зависимости и позволяет переиспользовать один и тот же логотип, текст или ссылку в нескольких конфигурациях без копипаста.
Пример конфигурации
Ниже — иллюстрация идеи (не точная схема хранения из production): структура документа, состав блоков и конкретные значения живут раздельно.
{ "layout": { "partnerCode": "PARTNER_A", "productCode": "TRAVEL", "version": 2, "blockNameOrder": ["HEADER", "TERMS", "FOOTER"], "onlyBlocks": true }, "blockData": { "blockName": "HEADER", "version": 2, "values": { "logo": "partner-a/logo-v2.png", "offerUrl": "https://example.org/offer", "companyName": "Partner A" } } }
Два кейса, ради которых всё и затевалось
Кейс 1. Исправить опечатку без большой миграции
Если это безопасная правка текста, её не нужно тащить через полную пересборку всего документа. Если изменение должно повлиять только на новые документы — делаем новую версию на нужном уровне модели; если структурное — оно идёт по каскаду.
Кейс 2. Новый логотип только для новых продаж
Бизнес хочет, чтобы новые документы печатались с новым логотипом, а старые — как раньше, без размножения полных копий шаблонов под каждого партнёра. Решение — поднять версию только того уровня, где реально произошло изменение (например, blockData), не создавая новый «монолитный» шаблон документа.
Почему ключевым оказался каскад версий
Самая опасная ситуация — частично обновлённая конфигурация: макет уже новый, блок ещё старый, значения уже обновлены, и поведение системы можно объяснить только ручным разбором связей.
Поэтому разные уровни версионируются независимо, но согласованно, а публикация становится полноценным процессом со статусами и правилами переходов.
Гибридный период и зачем нужен onlyBlocks
Переход к «конструктору» не происходит мгновенно: какое-то время сосуществуют старые и новые поколения. У нас параллельно жили три сценария:
Полностью старый макет.
Старый макет с точечным внедрением блоков.
Новый макет, который собирается только из блоков.
В этом контексте onlyBlocks — не просто оптимизация, а режим, который позволяет реально работать с блочной композицией и ускоряет цикл «поправил → посмотрел».
Как документ собирается на backend
На практике «конструктор PDF» — это серверный pipeline:
Получаем запрос на генерацию документа.
Берём исходный
.rptdesign(из кэша или Object Storage).Определяем, какой
layoutи какие версииblockDataдолжны участвовать в сборке.Находим в шаблоне точки вставки для блоков.
Подтягиваем XML нужных блоков и значения для них.
Собираем итоговый
.rptdesignи при необходимости кэшируем подготовленную версию.Передаём результат в BIRT runtime.
Возвращаем готовый PDF.
Условный псевдокод:
report = loadBaseTemplate(partnerCode, productCode, version) anchors = findBlockAnchors(report) blocks = loadBlocksWithValues(layoutVersion, blockDataVersions) preparedReport = injectBlocks(report, anchors, blocks) pdf = birtRuntime.render(preparedReport) return pdf
Управляемая публикация: как бизнес меняет документ безопасно
Если бизнес может свободно менять структуру, риски смещаются из «код-ошибок» в «ошибки конфигурации/контента». Чтобы удержать систему управляемой, мы ввели процесс и ограничения.
Процесс
Draft— бизнес создаёт черновик конфигурации и контента, собирает структуру, проверяет предпросмотр (в т.ч.onlyBlocks).Product review— продукт ревьюит визуальный результат и корректность (бренд / читабельность / логика документа).Publish— публикация создаёт версию, которая становится доступна для прод-генерации.
Guardrails
Так как обязательных блоков нет, важны автоматические проверки:
Валидация входной конфигурации: корректность
blockNameOrder, допустимость комбинаций версий, валидность ссылок/изображений и типов данных.Проверка совместимости версий
layout/block/blockDataна этапе публикации (матрица соответствий версий).Предсказуемый предпросмотр: бизнес видит итоговый PDF до публикации и не выпускает «сломанный» документ.
Импорт/экспорт и безопасность
Как только появляются версии, блоки, связанные значения, изображения и зависимости, становится критичным переносить конфигурацию между средами (DEV → TEST → PROD) без ручной сборки.
Поэтому в платформе появляется импорт/экспорт:
блоки едут вместе со связанными сущностями;
значения и изображения не теряются;
связи сохраняются;
макеты можно переносить отдельно.
Безопасность тоже перестаёт быть факультативной. Когда в системе есть XML, архивы и пользовательские файлы, нужны:
безопасный XMLпарсинг (защита от XXE);
контроль путей внутри архивов (zip slip);
валидация загружаемых файлов;
контроль целостности зависимостей при импорте.
Что измеряем, если цель — user-friendly, а не скорость
Если задача — делегировать изменения бизнесу, ключевые метрики должны быть продуктовыми:
Метрика |
Что показывает |
|---|---|
Lead time изменения |
От идеи/правки до опубликованной версии |
Доля self-service изменений |
Процент правок, сделанных бизнесом без участия разработки (по событиям draft / review / publish) |
Количество откатов |
И причины: ошибка данных, визуальная ошибка, неверный порядок блоков, несовместимость версий |
Правки после review |
Индикатор качества интерфейса и guardrails |
Заключение
Мы не отказались от BIRT и не переписывали рендеринг с нуля. Мы добавили предметный backend-слой, который превращает набор шаблонов в платформу: документ стал композицией версий макета, блока, данных блока и справочников значений, плюс правила публикации и переноса между средами.
Такой подход делает генерацию страховых полисов управляемой: изменения можно выпускать безопасно и предсказуемо, не превращая каждый новый вариант документа в очередную копию.
Если у вас тоже есть BIRT, Jasper или любая другая шаблонная платформа — и вы уже чувствуете, что модель «один кейс — один шаблон» больше не работает — возможно, вам нужен не новый рендерер, а нормальный слой композиции, версионирования и публикации поверх текущего движка.