Сейчас система спроектирована как single-source, tender-centric:
- одна основная сущность:
tender - канонический ключ вида
<source>:<source_tender_id> - procurement-поля вроде дедлайна, организатора, НМЦ, лотов, документов
Это хорошо работает для:
- Контур
- Seldon
Но новые источники уже другого типа:
- ЕГРЗ
- investment push
- LMG financing
Проблема не только в том, что у них “другие колонки”.
Проблема в том, что это разные типы сущностей.
Поэтому нам не стоит пытаться класть всё в одну таблицу tenders.
Примеры:
- Контур
- Seldon
Типичные данные:
- search hit
- tender detail
- results / protocols
- документы
- поля закупки:
- название
- заказчик
- организатор
- статус
- дедлайн
- НМЦ
- лоты
- объекты
Это естественно ложится в tender.
Пример:
- ЕГРЗ
Типичные данные:
- номер заключения экспертизы
- тип и результат экспертизы
- объект и адрес
- регион
- застройщик / техзаказчик / проектировщик
- сведения о ТПД / типовых решениях
- сведения об эффективности
- часто нет procurement-атрибутов:
- нет дедлайна закупки
- нет организатора закупки
- нет НМЦ
- нет тендерных документов
Это не tender.
Это скорее egrz_record или project_signal.
Пример:
- investment projects JSON push
Типичные данные:
- входящий JSON-лид
- данные по проекту / объекту
- организация
- ответ о дубле
- метаданные исходящего ERP payload
Это ближе к investment_project, чем к tender.
Пример:
- LMG financing
Типичные данные:
- строка сводного файла
- программа / объект / бюджет / финансирование
- часто вообще нет procurement identity
Это отдельный тип, например financing_item.
- Raw-данные источника должны сохраняться как есть.
- Для реестра и аналитики нужен общий слой.
- Source-specific поля не надо насильно унифицировать.
- Cross-source совпадения на первом этапе лучше хранить как
links, а не как жёсткий merge. - Документы и extraction должны быть привязаны к snapshot, но быть опциональными.
Одна загрузка / один batch / один run / один file import.
Примеры:
- один импорт CSV ЕГРЗ
- один batch webhook-запросов
- один scheduled run Контур
- одна ручная загрузка файла ЛМГ
Поля:
idsource_systemingest_mode(api,file,webhook,manual)file_namestorage_keymetadatacreated_at
Одна сырая запись из источника.
Поля:
idbatch_idsource_systemrecord_kindexternal_idsource_urlpayload_jsonpayload_hashreceived_at
Примеры record_kind:
search_hitdetail_payloadresults_payloadregistry_rowfile_rowwebhook_payload
Главная сущность для реестра, фильтров и аналитики.
Поля:
idcanonical_keyentity_kindtenderegrz_recordinvestment_projectfinancing_item
lead_channelkonturseldonegrzinvestmentlmg_financing
source_systemexternal_idcurrent_published_snapshot_idcurrent_titlecurrent_source_urlcurrent_org_namecurrent_org_inncurrent_region_codecurrent_industry_codecurrent_amount_rubcurrent_event_datecurrent_deadline_atsource_statusprocessing_statusrelevance_statusfirst_discovered_atlast_discovered_at
Смысл: это не “универсальный тендер”, а общая оболочка сущности.
Версионированное состояние сущности.
Поля:
identity_idversioncommon_data_jsonmaterial_change_hashpublication_statesnapshot_atcreated_at
Только procurement-поля:
- notification number
- дедлайн
- дата итогов
- НМЦ
- заказчик
- организатор
- лоты
- объекты
- tender-specific status
Только EGRZ-поля:
expertise_numberexpertise_result_idexpertise_typeexpertise_document_typeexpertise_result_typeobject_nameobject_addressdeveloper_infotechnical_customer_infoplanner_infoeconomy_efficiency_infoefficiency_order_numberefficiency_order_datetprtpr_listis_tprwork_type
Только investment-specific поля:
- inbound JSON lead fields
- duplicate-check state
- ERP payload metadata
Только financing-specific поля:
- source program
- funding attributes
- row-specific workbook fields
Документы, если они есть у конкретного snapshot.
Результаты парсинга документов.
Извлечённые факты.
Evidence-ссылки.
Скоринг и объяснение.
Важно: этот слой должен быть опциональным.
- у tender он обычно есть
- у public EGRZ его может не быть
- у investment push сначала может не быть
- модель при этом не ломается
Мягкие связи между сущностями.
Поля:
idfrom_entity_idto_entity_idlink_typepossible_same_projectconfirmed_same_projectsame_counterpartyderived_from
confidenceevidence_jsoncreated_at
Это минимальный и безопасный способ поддержать cross-source matching без тяжёлого merge.
Потому что ЕГРЗ, investment и financing не являются тендерами.
Если всё класть в tenders, мы быстро получим:
- семантически ложные поля
- много meaningless
null - странные статусы
- усложнение UI и аналитики
Потому что нам всё равно нужен:
- единый реестр
- единая аналитика
- единые фильтры
- единая операционная поверхность
Потому что он даёт:
- сохранение raw-правды
- одну общую registry/entity модель
- source-specific детализацию
- мягкий cross-source linkage
- разумный минимализм
Пример: Контур нашёл новый тендер.
Поток:
- Сохраняем raw API ответы в
source_records - Создаём или обновляем
entityentity_kind = tenderlead_channel = kontur
- Создаём
entity_snapshot - Создаём
tender_snapshot - Загружаем документы
- Запускаем parsing / extraction / scoring
- Публикуем snapshot
Результат:
- одна entity
- один typed tender snapshot
- документы и derived facts привязаны к snapshot
Пример: нашли запись реестра, но в Контуре/Селдоне ничего нет.
Поток:
- Сохраняем raw row в
source_records - Создаём
entityentity_kind = egrz_recordlead_channel = egrz
- Создаём
entity_snapshot - Создаём
egrz_snapshot - Документный pipeline не запускаем, если документов нет
- Делаем только лёгкое enrichment:
- регион
- отрасль
- нормализация организации
- signal classification
Результат:
- запись живёт в системе сама по себе
- это не “тендер-заглушка”
- позже её можно связать с tender, если он найдётся
Пример: сначала ЕГРЗ, потом тот же проект нашли в Контуре или Seldon.
Поток:
egrz_recordуже существует как отдельная entity- Позже приходит
tenderкак отдельная entity - Matching logic находит возможную связь
- Создаём
entity_link - Обе сущности пока остаются отдельными
Результат:
- нет premature merge
- нет потери source-specific смысла
- cross-source intelligence появляется через link
Это оптимальный минималистичный вариант для первого этапа.
Пример: investment project приходит webhook’ом.
Поток:
- Принимаем webhook
- Сохраняем payload в
source_records - Создаём
entityentity_kind = investment_projectlead_channel = investment
- Создаём
entity_snapshot - Создаём
investment_snapshot - Запускаем duplicate check
- Сохраняем результат и метаданные downstream response
Результат:
- real-time source ложится в ту же схему
- не нужен отдельный special-case storage path
Пример: LMG workbook или CSV ЕГРЗ.
Поток:
- Создаём
source_batch - Каждую строку сохраняем как
source_record - Строим
entity + entity_snapshot + typed snapshot - Если это full refresh, помечаем superseded / inactive старые сущности или snapshots
- Обновляем registry projection
Результат:
- file imports используют ту же core-модель, что API и webhook
- lineage batch-загрузки сохраняется
Оставить текущий Kontur pipeline почти как есть.
Добавить новые общие таблицы:
source_batchessource_recordsentitiesentity_snapshotsentity_links
Считать текущий procurement flow первым subtype:
- либо временно оставить текущие
tenders - либо постепенно перенести их в
tender_snapshots
Добавить новые typed subtypes:
egrz_snapshotsinvestment_snapshotsfinancing_snapshots
Для нашей текущей стадии наиболее разумная схема такая:
- raw source layer
- common entity registry
- typed source-specific snapshots
- soft links between entities
- optional document/extraction layer per snapshot
Это минимальная модель, которая:
- поддерживает несколько разных семей источников
- не ломает семантику данных
- остаётся понятной для команды
- не превращается в тяжёлую MDM-систему
- позволяет постепенно расти от procurement-only модели к multi-source системе