Как мы перестали размножать BIRT‑шаблоны и собрали блочную платформу self‑service генерации страховых полисов в PDF

от автора

Я 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:

  1. Получаем запрос на генерацию документа.

  2. Берём исходный .rptdesign (из кэша или Object Storage).

  3. Определяем, какой layout и какие версии blockData должны участвовать в сборке.

  4. Находим в шаблоне точки вставки для блоков.

  5. Подтягиваем XML нужных блоков и значения для них.

  6. Собираем итоговый .rptdesign и при необходимости кэшируем подготовленную версию.

  7. Передаём результат в BIRT runtime.

  8. Возвращаем готовый 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 или любая другая шаблонная платформа — и вы уже чувствуете, что модель «один кейс — один шаблон» больше не работает — возможно, вам нужен не новый рендерер, а нормальный слой композиции, версионирования и публикации поверх текущего движка.

ссылка на оригинал статьи https://habr.com/ru/articles/1032464/