Перейти к содержанию

Платформа для ботанических садов (SaaS) - всё вместе

Эта страница собирается из компактных разделов проектной документации. Отдельные файлы разделов остаются источником истины; эта страница предназначена для последовательного чтения, ревью и экспорта в стиле PDF.

Содержание

Краткое описание

Статус

MVP / проведение пилота

Роль

Сооснователь, технический владелец архитектуры, системный проектировщик

Стек

Java 21, Spring Boot, PostgreSQL/PostGIS, MinIO/S3-compatible storage, Angular, OpenLayers, Docker

Ценность проекта

Для ЦА (ботанические сады, питомники, селекционеры, держатели научных коллекций и частных собраний растений):

  • переход от разрозненных Excel/локальных БД к единой web-платформе;

  • единый таксономический слой как основа сопоставимости коллекционных данных;

  • управляемое раскрытие данных, публичные страницы и QR-сценарии;

  • база для сетевых сценариев обмена между организациями.

Для профессионального профиля:

  • проектирование и реализация SaaS от доменной идеи до MVP и пилотного запуска;

  • работа с multi-tenancy, RBAC, доменной моделью данных, API, GIS, AI-assisted импортом, подготовкой публичных данных и приведение в соответствие законодательным нормам (152-ФЗ);

  • осознанное и экономичное применение AI без передачи архитектурных решений модели.

Что демонстрирует

Этот проект демонстрирует мои способности:

  • работать на стыке системного анализа, системного дизайна, имплементации и деплоя.

  • довести идею до запуска системы с несколькими зонами доступности и дорожной картой масштабирования

  • стратегически планировать имплементацию SaaS малого-среднего масштаба

  • безопасно и контролируемо применять LLM-генерацию кода для имплементации фичей

Обзор

Botanical SaaS - мультитенантная SaaS-платформа для ботанических садов, питомников, селекционеров, научных коллекций и частных собраний растений.

Система заменяет разрозненные Excel-файлы, локальные базы данных и устаревшие desktop-инструменты единой web-платформой для управления живыми коллекциями растений, научной таксономией, геоданными, публичными страницами организаций, QR-этикетками, импортом, списками и контролируемым обменом данными.

Моя зона ответственности включала доменное моделирование, backend-архитектуру, проектирование модели доступа, проектирование структуры данных и API, техническую документацию, backend-реализацию и подготовку системы к cloud-ready deployment.

Контекст и проблема

Контекст

У целевой аудитории уже есть данные, но они рассыпаны по бумажным журналам, Excel, Word/PDF, Access/FileMaker, локальным БД, старым desktop-системам и частично по специализированным решениям. Поэтому проект не продаёт "учёт с нуля", а продаёт переход из хаотичного, несопоставимого и плохо публикуемого состояния в структурированную систему.

Для ботанических садов грубая оценка такая: Excel остаётся главным операционным слоем у 35–50%, бумага значима у 10–25%, локальные БД и desktop-системы у 20–30%, современные web/cloud-системы у 5–15%. Специализированные системы типа IrisBG, BG-BASE, BRAHMS и Hortis закрывают профессиональный верх рынка, но не доминируют глобально.

BGCI не является прямым конкурентом как рабочая система учёта. BGCI важен как глобальный справочный и агрегационный слой: GardenSearch показывает организации, PlantSearch агрегирует taxon-level данные из коллекций. Но он не заменяет повседневный учёт экземпляров, перемещений, выпадов, фото, GIS, QR, внутренние роли, импорт и отчётность.

По сегментам картина различается. Частные коллекционеры в основном используют Excel, Google Sheets, бумагу, Notion/Airtable или простые hobby-инструменты. Селекционеры ведут учёт крестов, сеянцев, линий и испытаний, часто в Excel или внутренних базах. Регистраторы культиваров работают с таблицами, PDF, локальными БД и публичными регистрами. Музеи сильнее цифровизированы в preserved collections, но живые коллекции часто учитываются ближе к ботсадам. Питомники чаще имеют ERP/web-учёт, но их фокус - склад, продажи и партии, а не научная коллекция.

Главная боль рынка - несопоставимость данных. В Excel один и тот же таксон может быть записан латиницей, с автором, без автора, по-русски, через синоним, с опечаткой или старым названием. Поэтому автоматический обмен, поиск, отчётность и сравнение коллекций почти невозможны без единого taxon layer.

Проблема

Ботанические коллекции часто ведутся в разрозненных таблицах, локальных базах и внутренних документах отдельных организаций. Это затрудняет:

  • поддержание консистентных записей о растениях;
  • привязку растений к проверенной научной таксономии;
  • отслеживание местоположения растений и изменений их состояния;
  • управление доступом между подразделениями и организациями;
  • безопасный импорт исторических данных;
  • публичную публикацию выбранных данных о коллекции;
  • обмен информацией между учреждениями и коллекционерами (обмен семенным фондом в рамках повышения биоразнообразия видов).

Botanical SaaS решает эту проблему как единая операционная платформа для живых коллекций растений.

Цели, требования и ограничения

Цели и нецели

Основные цели проекта

  • Перевести учёт живых коллекций растений из Excel, бумажных журналов и локальных баз в единую web-систему.
  • Обеспечить научно корректную привязку записей к глобальному источнику таксономии, культиварам и грексам.
  • Дать ботаническим садам, коллекционерам, музеям и питомникам инструмент для учёта экземпляров, мест размещения, фото, статусов, поступлений и выбытий.
  • Реализовать GIS-ядро: карта территории, участки, оранжереи, грядки, точки растений и полигоны их размещения.
  • Упростить миграцию существующих данных через импорт Excel, сопоставление столбцов, проверку названий и отчёты по ошибкам.
  • Создать публичную витрину организаций: страницы садов, публичные списки, карточки растений и QR-этикетки.
  • Поддержать избирательное раскрытие данных.
  • Построить сетевой слой для ботанического сообщества: списки обмена, wishlists и автоматическое сопоставление желаемых таксонов с доступным материалом.
  • Дать администрациям и кураторам отчётность по коллекционным фондам, подразделениям, таксонам, поступлениям и выбытиям.
  • Сформировать основу для долгосрочного контура культиваров, регистраторов и отраслевой стандартизации данных.
  • Обеспечить генерацию этикеток с QR-кодами, ведущими на карточку растения в системе.

Что не входит в проект

  • Не является заменой BGCI как глобального справочника ботанических садов и агрегатора данных уровня биологических таксонов.
  • Не является гербарной системой полного цикла для гербарных коллекций, музейных фондов.
  • Не является ERP для коммерческих питомников с фокусом на продажи, склад, закупки, производство, кассы и бухгалтерию.
  • Не является CRM, тасктрекером, системой документооборота или корпоративным порталом организации.
  • Не включает физическую печать, изготовление, установку и обслуживание QR-этикеток на территории организации.
  • Не решает юридическую регистрацию культиваров вместо официальных ICRA/регистраторов; система может только поддерживать цифровой контур данных и заявок.
  • Не гарантирует автоматическую очистку всех исторических данных без участия эксперта: спорные названия, синонимы, сорта и ошибки требуют валидации.
  • Не раскрывает приватные коллекции по умолчанию; публикация данных остаётся управляемым решением владельца.
  • Не является полноценной BI/аналитической платформой общего назначения; отчётность ограничена задачами живых коллекций, поступления и выбытия растений.
  • Не включает на первом этапе тяжёлый enterprise-контур: биллинг, SLA, multi-region replication, госзакупки, реестр ПО, кастомные интеграции и отдельные инсталляции под каждого клиента.

Бизнес-Требования

  • BR-001. Централизовать учёт живых коллекций растений.
    Проект должен предоставить ботаническим организациям, коллекционерам и смежным участникам рынка единую web-систему для замены разрозненных Excel-файлов, бумажных журналов, Word/PDF-документов и локальных баз.

  • BR-002. Повысить научную корректность данных о растениях.
    Проект должен обеспечить привязку записей о растениях к авторитетной таксономической базе, поддерживать иерархию семейство -> род -> вид, а также отдельный контур культиваров и грексов.

  • BR-003. Снизить стоимость и сложность миграции существующих коллекционных данных.
    Проект должен позволить импортировать исторические данные из Excel и других табличных источников с сопоставлением столбцов, нормализацией значений, проверкой названий и отчётами по ошибкам.

  • BR-004. Обеспечить операционный учёт экземпляров растений.
    Проект должен поддерживать ведение карточек конкретных экземпляров растений с инвентарными номерами, статусами, происхождением, формой поступления, местом размещения, фото, пользовательскими полями и историей изменений (полный список атрибутов согласован отдельно).

  • BR-005. Связать коллекционные данные с пространственным контекстом.
    Проект должен дать организациям возможность картировать территорию, подразделения, участки, оранжереи, грядки и отдельные растения с использованием точек, полигонов и интерактивной карты.

  • BR-006. Дать организациям публичную цифровую витрину.
    Проект должен позволить создавать публичные страницы организаций, публичные карточки растений, публичные списки и карту коллекций без необходимости разрабатывать отдельный сайт.

  • BR-007. Связать физические растения с цифровыми карточками.
    Проект должен поддерживать генерацию QR-этикеток, ведущих посетителя или сотрудника к публичной или внутренней карточке растения.

  • BR-008. Поддержать управляемое раскрытие данных.
    Проект должен позволить владельцам коллекций управлять видимостью данных: приватно, для зарегистрированных пользователей, для сообщества или публично.

  • BR-009. Создать сетевой слой обмена между участниками ботанического сообщества.
    Проект должен поддерживать списки коллекции, списки обмена и списки желаний, чтобы участники могли сопоставлять желаемые таксоны с доступным материалом у других организаций, соуществляя обмен.

  • BR-010. Обеспечить отчётность для кураторов и администрации.
    Проект должен предоставлять отчёты по составу коллекций, таксонам, подразделениям, поступлениям, выбытиям, спискам и состоянию коллекционных фондов.

... В публичной версии приведён сокращённый фрагмент требований. Полная детализация не раскрывается из-за объёма и продуктовых ограничений.

Правила и Ограничения

Business Rules

  • RULE-001. Данные организации закрыты по умолчанию. Коллекционные данные новой организации считаются приватными, пока владелец явно не изменит уровень видимости.

  • RULE-002. Владелец данных управляет публичностью. Организация самостоятельно определяет, какие растения, списки, фото, координаты и страницы доступны публично, сообществу или только внутренним пользователям.

  • RULE-003. Пользователь не может получать доступ к данным чужой организации без разрешения. Доступ к растениям, спискам, местам, фото и импорту должен ограничиваться организацией пользователя и выданными ему ролями.

  • RULE-004. Species-записи должны быть привязаны к справочной таксономии. Пользователи не должны создавать произвольные видовые записи без связи с авторитетным таксономическим справочником.

  • RULE-005. Локальные культивары принадлежат организации-создателю. Культивар, созданный внутри организации, считается локальным и не становится глобально доступным автоматически (только через процедуру регистрации уполномоченным регистратором).

... В публичной версии приведён сокращённый фрагмент бизнес-правил. Полная детализация не раскрывается из-за объёма и продуктовых ограничений.

Ограничения

  • CON-001. Система должна быть web/SaaS-решением. Продукт проектируется как web-платформа с централизованным доступом, а не как desktop-приложение.

  • CON-004. Excel должен поддерживаться как основной формат миграции. Система должна учитывать, что исходные данные целевой аудитории чаще всего находятся в Excel-файлах.

  • CON-005. Исходные данные могут быть грязными и неоднородными. Импорт должен учитывать опечатки, синонимы, локальные названия, неполные значения, нестандартные столбцы и исторические форматы учёта.

  • CON-006. GIS-точность ограничена качеством исходных данных. Система может хранить и отображать точки и полигоны, но фактическая точность зависит от действий пользователя и качества полевых данных.

... В публичной версии приведён сокращённый фрагмент ограничений. Полная детализация не раскрывается из-за объёма и продуктовых ограничений.

Роль и обязанности

Моя роль

Я выступал как сооснователь и технический владелец архитектуры системы.

Моя работа включала:

  • перевод экспертных знаний предметной области в структурированную SaaS-модель;
  • проектирование backend-архитектуры, API, структур данных, доменной модели;
  • реализацию backend-функциональности и интеграционной логики;
  • проектирование RBAC с учетом прав доступа в контексте организации (тенанта);
  • моделирование экземпляров растений, таксономии, мест размещения, списков, фотографий, импортов и публичных страниц;
  • подготовку архитектурной документации, C4-диаграмм и ADR;
  • согласование технических решений с экспертом предметной области;
  • использование AI-assisted development tools для ускорения реализации при сохранении ручного контроля над архитектурой, моделью данных, API-контрактами, ревью и deployment-решениями.

Применение ИИ

Проект разрабатывался с использованием AI.

LLM применялись для ускорения реализации, генерации шаблонного кода и быстрых итераций. Ключевые решения оставались под ручным контролем:

  • интерпретация требований;
  • доменное моделирование;
  • архитектурные решения;
  • границы данных;
  • модель доступа;
  • код ревью;
  • дебаг;
  • решения по развертыванию;
  • техническая документация.

Модель системы

Доменная модель

Научная таксономия

Записи о растениях связаны с централизованным таксономическим слоем (не раскрывается публично).

Модель поддерживает семейства, роды, виды, культивары, грексы, глобальные и локальные культивары, принадлежащие отдельным тенантам.

Модель данных

High-Level Data Model

erDiagram
    ORGANIZATION ||--o{ ORG_UNIT : contains
    ORGANIZATION ||--o{ PLANT : owns
    ORGANIZATION ||--o{ PLACE : manages
    ORGANIZATION ||--o{ PLANT_LIST : maintains
    ORGANIZATION ||--o{ PUBLIC_PAGE : publishes

    ORG_UNIT ||--o{ PLANT : curates
    PLACE ||--o{ PLACE : contains
    PLACE ||--o{ PLANT : locates

    PLANT }o--|| SYSTEM_TAXON : "required taxon_id"
    PLANT ||--o{ PHOTO : documents
    PLANT }o--o{ PLANT_LIST : included_in

    SYSTEM_TAXON ||--o| GLOBAL_TAXON : "species subtype"
    SYSTEM_TAXON ||--o| CULTIVATED_ENTITY : "cultivar/grex subtype"

    PUBLIC_PAGE ||--o{ PLANT_LIST : exposes
    PUBLIC_PAGE ||--o{ PLANT : exposes_selected

    ORGANIZATION {
        uuid id PK
        string public_name
        string slug
        string visibility
    }

    ORG_UNIT {
        uuid id PK
        uuid organization_id FK
        uuid parent_id FK
        string name
    }

    PLANT {
        uuid id PK
        uuid organization_id FK
        uuid org_unit_id FK
        uuid place_id FK
        uuid taxon_id FK "mandatory"
        string accession_number
        string individual_code
        string status
        string provenance_type
        geometry point
        jsonb custom_fields
    }

    SYSTEM_TAXON {
        uuid id PK
        string display_name
        string normalized_name
        string taxon_type
        string rank
    }

    GLOBAL_TAXON {
        uuid id PK
        uuid system_taxon_id FK
        string id
        string ipni_id
        string family
        string genus
        string species_epithet
        string scientific_name
    }

    CULTIVATED_ENTITY {
        uuid id PK
        uuid system_taxon_id FK
        string genus
        string cultivar_or_grex_name
        string breeder_name
        string registrar_name
        int registration_year
        string visibility_scope
    }

    PLACE {
        uuid id PK
        uuid organization_id FK
        uuid parent_id FK
        string name
        geometry point
        geometry polygon
    }

    PLANT_LIST {
        uuid id PK
        uuid organization_id FK
        string name
        string list_type
        string visibility
    }

    PHOTO {
        uuid id PK
        uuid PLANT_id FK
        string caption
        string photo_tag
        boolean publication_allowed
    }

    PUBLIC_PAGE {
        uuid id PK
        uuid organization_id FK
        string slug
        boolean visible
    }

Ключевая идея модели

Центральная сущность системы - Plant, цифровой двойник конкретного растения в живой коллекции. Это не просто строка справочника и не абстрактный вид, а учётная запись конкретного экземпляра: с инвентарным номером, статусом, местом размещения, принадлежностью к организации, фотографиями, списками и публичным представлением.

Обязательный атрибут taxon_id является ядром модели. Он связывает каждый экземпляр растения с единой корневой сущностью SystemTaxon. Благодаря этому все операционные сценарии - учёт, импорт, поиск, списки, обмен, QR-страницы, отчётность и публичная карта -работают не с произвольным текстовым названием растения, а с устойчивым идентификатором таксона.

SystemTaxon выступает общей корневой сущностью для двух крупных таксономических контуров:

  • globalTaxon - видовые таксоны, поступающие из авторитетного каталога.
  • CultivatedEntity - культивары и грексы, которые могут быть признанными глобальными записями, поступающими от регистраторов, либо локальными записями, созданными внутри конкретной организации.

Такой подход позволяет пользователю выбирать растение из единого taxon lookup, не разделяя искусственно поиск по видам и поиск по культиварам. Для пользователя это выглядит как единый справочник растений, а внутри системы сохраняется различие между научными видовыми таксонами, глобальными культиварами, грексами и локальными культиварами организации.

Архитектурная ценность решения состоит в том, что все коллекционные данные становятся сопоставимыми между организациями. Один и тот же taxon_id может использоваться в карточках растений, списках коллекций, wishlists, списках обмена, публичных страницах и отчётах. Это создаёт основу для сетевых сценариев: система может сопоставлять, какие таксоны одна организация ищет, а другая готова передать, не полагаясь на нестабильные текстовые названия, синонимы, локальные варианты и опечатки.

В портфолио модель показана верхнеуровнево. Внутренние вспомогательные сущности, механизмы разграничения видимости, таблицы доступа, workflow глобализации культиваров и технические детали реализации намеренно опущены.

API-контракты

Названия конечных точек и атрибуты намеренно изменены для публичного пространства.

Подход к проектированию API

Backend предоставляет REST/JSON API для управления живыми коллекциями растений в мульти-тенантной SaaS-среде. API-контракты организованы вокруг доменных возможностей: учёт растений, поиск по таксономии, массовые операции, импорт, публичные страницы, доступ к медиа и сценарии обмена.

Безопасность обеспечивается на нескольких уровнях:

  • JWT-аутентификация через Spring Security.
  • Method-level authorization для чувствительных операций.
  • Изоляция тенантов через контекст организации / корневого тенанта.
  • Фильтрация данных на уровне владения тенантом.
  • Bean Validation для request DTO.
  • Контролируемые public DTO для анонимных endpoints.
  • Soft delete для обратимых удалений.
  • Централизованная обработка ошибок со стандартными HTTP-статусами.

1. Поиск экземпляров растений

Назначение

Получить постраничный список экземпляров растений, доступных текущему пользователю в активном контексте организации.

Контракт
GET /api/inventory/plants

Query Parameters

keyword          необязательный текстовый поиск
organization     необязательный контекст тенанта / корневой организации
unit             необязательный фильтр по подразделению или коллекционной единице
list             необязательный фильтр по списку
recursive        необязательный флаг поиска по вложенным подразделениям
filter           необязательные фасетные фильтры
page             параметр пагинации
size             параметр пагинации
sort             параметр сортировки
Response
{
  "items": [
    {
      "id": "uuid",
      "displayName": "string",
      "taxon": "summary",
      "inventoryCode": "string",
      "status": "string",
      "location": "summary",
      "visibility": "string"
    }
  ],
  "page": {
    "number": 0,
    "size": 25,
    "totalElements": 0
  }
}
Безопасность
  • Требуется аутентифицированный пользователь.
  • Результат ограничивается tenant-контекстом, доступным текущему пользователю.
  • Query specifications применяют tenant-фильтрацию до возврата данных.
  • Filter expressions разбираются в allowlisted-критерии, а не выполняются как сырые динамические запросы.
  • Пагинация и сортировка используют framework-level pageable parameters.

2. Получение деталей экземпляра растения

Назначение

Получить один экземпляр растения как цифровую запись реального растения в живой коллекции.

Контракт
GET /api/inventory/plants/{plantId}
Response
{
  "id": "uuid",
  "taxon": {
    "id": "uuid",
    "displayName": "string",
    "type": "species | cultivar | grex"
  },
  "inventory": {
    "accessionNumber": "string",
    "individualCode": "string",
    "status": "string"
  },
  "organization": "summary",
  "location": "summary",
  "photos": ["summary"],
  "customFields": {}
}
Безопасность
  • Требуется аутентифицированный пользователь.
  • Доступ проверяется по принадлежности растения тенанту и членству пользователя в организации.
  • Внутренние поля не раскрываются через публичные контракты.
  • Несуществующие или недоступные ресурсы возвращаются через стандартные error responses.

3. Создание экземпляра растения

Назначение

Создать новый экземпляр растения в живой коллекции организации.

Контракт
POST /api/inventory/plants
Request
{
  "taxonId": "uuid",
  "organizationUnitId": "uuid",
  "placeId": "uuid",
  "inventoryData": {},
  "status": "string",
  "customFields": {}
}
Response
201 Created
{
  "id": "uuid",
  "displayName": "string",
  "taxon": "summary",
  "inventoryCode": "string",
  "status": "string"
}
Безопасность
  • Требуется аутентифицированный пользователь.
  • Требуется административный или редакторский доступ к целевому подразделению организации.
  • Request body валидируется до выполнения доменной логики.
  • taxonId обязателен и должен ссылаться на разрешённый вид, культивар или грекс, видимый в текущем tenant-контексте.
  • Создание передаётся в service layer после авторизации и валидации.

4. Обновление экземпляра растения

Назначение

Обновить существующий экземпляр растения: инвентарные данные, таксономическую привязку, статус или организационное размещение.

Контракт
PUT /api/inventory/plants/{plantId}
Request
{
  "taxonId": "uuid",
  "organizationUnitId": "uuid",
  "placeId": "uuid",
  "inventoryData": {},
  "status": "string",
  "customFields": {}
}
Response
{
  "id": "uuid",
  "displayName": "string",
  "taxon": "summary",
  "status": "string",
  "location": "summary"
}
Безопасность
  • Требуется аутентифицированный пользователь.
  • Требуется write access к существующей записи растения.
  • Если растение перемещается в другое подразделение, должен быть проверен доступ и к текущему, и к целевому контексту.
  • Недопустимые попытки перемещения отклоняются с ошибкой доступа.
  • Валидация запроса выполняется через типизированные request DTO и Bean Validation.

5. Soft Delete экземпляра растения

Назначение

Переместить запись растения в корзину без физического удаления из базы данных.

Контракт
DELETE /api/inventory/plants/{plantId}
Response
204 No Content
Безопасность
  • Требуется аутентифицированный пользователь.
  • Требуется административный доступ к организационному контексту растения.
  • Удаление по умолчанию является обратимым.
  • Soft-deleted записи исключаются из обычных запросов.
  • Permanent delete ограничен повышенными platform-level ролями.

6. Массовые операции с экземплярами растений

Назначение

Выполнить массовые действия над выбранными экземплярами растений: перемещение, обновление, клонирование, генерация данных для этикеток или отметка этикеток как напечатанных.

Контракты

POST /api/inventory/plants/batch/move
POST /api/inventory/plants/batch/update
POST /api/inventory/plants/batch/clone
POST /api/inventory/plants/batch/label-data
POST /api/inventory/plants/batch/mark-printed
Request
{
  "plantIds": ["uuid"],
  "operationPayload": {}
}
Response
{
  "successCount": 0,
  "failureCount": 0,
  "results": [
    {
      "id": "uuid",
      "status": "success | failed",
      "message": "string"
    }
  ]
}
Безопасность
  • Требуется аутентифицированный пользователь.
  • Каждое затронутое растение должно быть проверено на tenant ownership и права пользователя.
  • Batch operations не должны обходить per-resource authorization.
  • Для операций, где часть записей может не пройти валидацию или авторизацию, поддерживается partial success.
  • Входные коллекции валидируются до обработки.

7. Получение справочников для форм растений

Назначение

Вернуть контролируемые справочные значения, используемые в формах растений: жизненные статусы, условия выращивания, формы поступления и другие значения.

Контракт
GET /api/inventory/dictionaries/{dictionaryType}
Response
[
  {
    "id": "string",
    "code": "string",
    "label": "string"
  }
]
Безопасность
  • Для внутренних справочников требуется аутентифицированный пользователь.
  • Ответы справочников содержат только безопасные reference data.
  • Значения контролируются платформой или tenant-конфигурацией, а не произвольным free text.

8. Поиск таксона

Назначение

Предоставить единый поиск по видам, культиварам и грексам при создании или редактировании экземпляра растения.

Контракт
GET /api/taxonomy/search
Query Parameters
query       поисковая строка
type        необязательный фильтр по типу таксона
page        параметр пагинации
size        параметр пагинации
Response
{
  "items": [
    {
      "id": "uuid",
      "displayName": "string",
      "taxonType": "species | cultivar | grex",
      "source": "reference | global | local"
    }
  ]
}
Безопасность
  • Глобальные reference species доступны всем тенантам.
  • Глобальные cultivated entities доступны всем тенантам.
  • Локальные cultivated entities видимы только разрешённым tenant-контекстам.
  • API скрывает внутреннюю механику видимости и отдаёт только selectable taxon summaries.

9. Workflow умного импорта

Назначение

Импортировать существующие коллекционные данные из таблиц через staged workflow.

Контракты
POST /api/import/sessions
POST /api/import/sessions/{sessionId}/sheet
POST /api/import/sessions/{sessionId}/mapping
POST /api/import/sessions/{sessionId}/resolve
POST /api/import/sessions/{sessionId}/execute
GET  /api/import/sessions/{sessionId}/status
GET  /api/import/sessions/{sessionId}/error-report
Workflow
Загрузка файла
→ Выбор листа
→ Сопоставление столбцов
→ Разрешение значений
→ Подтверждение неоднозначных совпадений
→ Выполнение импорта
→ Просмотр результатов
Response Example
{
  "sessionId": "uuid",
  "status": "uploaded | mapped | resolving | ready | executing | completed | failed",
  "progress": {
    "processedRows": 0,
    "totalRows": 0
  }
}
Безопасность
  • Требуется аутентифицированный пользователь.
  • Требуется tenant-контекст и permission на импорт.
  • Загруженные файлы хранятся вне реляционной базы данных.
  • Import sessions являются tenant-scoped.
  • Неоднозначные совпадения требуют подтверждения пользователя.
  • Некорректные строки отражаются в отчёте, но не блокируют все валидные строки.
  • Для долгих этапов импорта используются background jobs.

10. Публичная страница растения

Назначение

Предоставить безопасное публичное представление записи растения, обычно используемое как цель QR-этикетки.

Контракт
GET /api/public/plants/{publicSlug}
Response
{
  "displayName": "string",
  "taxon": "public summary",
  "organization": "public summary",
  "photos": ["public media reference"],
  "publicDescription": "string"
}
Безопасность
  • Аутентификация не требуется.
  • Возвращаются только public DTO.
  • Исключаются внутренние идентификаторы, tenant metadata, приватные заметки, внутренние инвентарные поля и закрытые фотографии.
  • Требуются публичные настройки видимости у организации-владельца и контекста растения / списка.
  • Доступ к медиа использует short-lived signed URLs или эквивалентный контролируемый механизм доставки.

11. Поиск совпадений для обмена

Назначение

Найти потенциальные совпадения между wishlist одной организации и exchange lists других организаций.

Контракт
GET /api/exchange/matches/{wishlistId}
Response
{
  "wishlist": "summary",
  "matches": [
    {
      "taxon": "summary",
      "sourceOrganization": "public or shared summary",
      "availableMaterial": "summary",
      "visibility": "string"
    }
  ]
}
Безопасность
  • Требуется аутентифицированный пользователь.
  • Пользователь должен иметь доступ к исходному wishlist.
  • Candidate matches ограничены данными, явно расшаренными для обмена или видимыми в соответствующем community scope.
  • Matching основан на taxon identifiers, а не на raw text names.
  • Private collection data не раскрывается через exchange API.

Паттерн обработки ошибок

{
  "status": 400,
  "code": "VALIDATION_ERROR",
  "message": "Request validation failed",
  "details": []
}

Типовые HTTP-статусы:

200 OK                  успешное чтение или обновление
201 Created             ресурс создан
204 No Content          успешное удаление
400 Bad Request         некорректный ввод или ошибка доменной валидации
403 Forbidden           пользователь аутентифицирован, но доступ запрещён
404 Not Found           ресурс отсутствует или не видим пользователю
409 Conflict            конфликт состояния или ресурс используется
429 Too Many Requests   превышен rate limit

Summary по безопасности

API-дизайн сочетает framework-level и domain-level protection:

  • Spring Security аутентифицирует запросы и формирует контекст пользователя.
  • JWT несёт идентичность, а роли и права резолвятся на серверной стороне.
  • Method-level проверки защищают операции записи и административные действия.
  • Tenant-scoped запросы предотвращают утечку данных между организациями.
  • DTO validation блокирует некорректные запросы до выполнения бизнес-логики.
  • Public endpoints используют отдельные DTO с ограниченным набором полей.
  • Soft delete защищает от случайной потери данных.
  • Операции уровня всей платформы отделены от операций тенантов.
  • Audit и login events обеспечивают трассируемость чувствительных действий.

Архитектура и интеграции

Архитектура

Контейнерная диаграмма (C4 Container)

    C4Container
    title Диаграмма контейнеров для Botanical SaaS MVP

    Person(user, "Пользователь")

    System_Boundary(sys, "Система") {
      Container(spa, "Single Page Application", "Angular 21, OpenLayers", "Пользовательский интерфейс") 
      Container(static, "Frontend Static", "nginx", "Контейнер для хранения<br/> статических frontend-файлов")
      Container(backend, "Backend Business Logic", "Spring Boot", "Бизнес-логика")
      ContainerDb(reldb, "Relational DB", "PostgreSQL + PostGIS", "Экземпляры растений, списки,<br/> пользователи, RBAC-роли")
      ContainerDb(objStore, "Object Storage", "MinIO", "Фотографии и мультимедиа.<br/> Файлы импорта коллекций")
    }

    Boundary(ext, "Внешние системы", "") {
      Container_Ext(global, "global", "Таксономический справочник")
      Container_Ext(vernacular, "WikiData", "Справочник народных названий<br/> растений")
      Container_Ext(llm, "LLM", "Интеллектуальное распознавание<br/> видов")
    }

  Rel(user, spa, "Использует для<br/> управления коллекциями растений")

  Rel(spa, static, "Получает Angular<br/> static UI bundles", "HTTPS")
  Rel(spa, backend, "Отправляет API-вызовы", "HTTPS REST")

  Rel(backend, reldb, "Читает/записывает данные", "SQL")
  Rel(backend, objStore, "Загружает/читает медиа", "S3 API")

  Rel(backend, global, "Запрашивает таксоны/культивары,<br/> записывает культивары", "HTTPS/REST")
  Rel(backend, vernacular, "Получает названия видов<br/> на национальных языках", "HTTPS/REST")
  Rel(backend, llm, "Использует LLM API<br/> для получения подсказок по названиям видов", "HTTPS/OpenAI API compatible")

  UpdateLayoutConfig($c4ShapeInRow="5", $c4BoundaryInRow="3")

Потоки интеграции

Импорт каталога таксономии

Система включает ручной механизм обновления внутреннего справочника таксонов из выгрузки xls с сохранением идентификаторов.

Пополнение национальных названий таксонов

Система включает автоматический механизм пополнения внутреннего справочника народных названий растений из открытых источников с учетом ограничений публичного API.

Smart Import

Платформа включает мастер импорта XLS для переноса существующих коллекций растений в систему.

Поток поддерживает загрузку файла, выбор листа, сопоставление колонок (в том числе с помощью ИИ), определение значений, нечеткое совпадение, асинхронную обработку, построчные результаты, экспорт Excel с ошибочными строками.

Для сопоставления названий колонок атрибутам сущностей системы, а также для более точного распознавания вида, культивара или перечисления(enum'а) предусмотрена интеграция с LLM и легкая обвязка(harness). Подтверждение распознавания (если оно не было 100%) осуществляется пользователем.

Безопасность, качество и эксплуатация

Модель безопасности и доступа

Мультитенантность и контроль доступа

Платформа использует soft multi-tenancy model на базе root organization unit pattern.

Пользователь может состоять в нескольких организациях и иметь разные роли в зависимости от текущего организационного контекста. Проверки доступа применяются на уровне сервисов, репозиториев, API и UI.

Публичная витрина

Организации могут публиковать выбранные данные через публичные страницы, публичные карточки растений, публичные списки, QR-label targets и глобальную карту.

Публичный слой использует отдельные DTO и visibility rules, чтобы не раскрывать внутренние поля и данные тенанта.

Двойной контур RBAC

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

Нефункциональные требования

Ключевые атрибуты качества

Атрибут Сценарий Подход
Security Пользователь организации A не должен получить данные организации B tenant-scoped queries, RBAC, public DTO, integration tests
Reliability Зависший импорт не должен блокировать систему background jobs, stale detection, retry/manual restart
Maintainability Доменная модель активно меняется на MVP-стадии modular monolith, package boundaries, DTO/service layers
Performance Поиск по справочнику таксонов должен оставаться приемлемым при росте каталога indexes, normalized names, pagination, fuzzy matching strategy
Operability Сбой VPS не должен приводить к полной потере данных backups, restore plan, migration path

Режимы отказа

Риск Последствие Митигирующая мера
ошибка tenant filtering утечка данных тесты, AccessControlService, scoped queries
падение VPS недоступность сервиса backup, restore plan, migration path
потеря MinIO volume потеря фото/импортов object storage backup
зависший import job блокировка импорта job lifecycle, stale detection
public DTO leakage раскрытие внутренних данных отдельные DTO, visibility rules

Оценка масштаба и стоимости

Основные драйверы нагрузки

  • количество организаций;
  • количество экземпляров растений;
  • количество фото на растение;
  • размер Excel-импортов;
  • частота Smart Import;
  • объём публичного трафика на карты, QR-страницы и изображения;
  • объём справочника таксонов.

Основные драйверы стоимости

  • VPS / compute;
  • PostgreSQL/PostGIS storage;
  • object storage для фото и импортов;
  • резервные копии;
  • LLM calls для Smart Import;
  • трафик публичных страниц и изображений;
  • мониторинг и хранение логов.

Масштабные уровни

См. Дорожную карту

Решения, компромиссы и риски

Ключевые решения

Разделение frontend и backend

Система использует отдельный Angular frontend и Spring Boot backend API. Это снижает связность, позволяет независимо развивать UI и backend-логику, а также создает основу для будущих клиентских каналов.

Root-unit soft multi-tenancy

Каждый tenant представлен корневым organizational unit. Tenant-scoped entities содержат root_unit_id, а доступ ограничивается через repositories, specifications, services и API-level checks.

Context-aware authorization

Права доступа зависят не только от глобальной роли пользователя, но и от текущей организации и выбранного организационного контекста.

Это соответствует B2B-сценариям, где один и тот же пользователь может иметь разные права в разных организациях или подразделениях.

PostGIS как часть доменной модели

Местоположение растений, участки сада, оранжереи, грядки и полигоны моделируются как spatial data, а не как вторичный map overlay.

Гибридная модель текущего состояния и истории

Система разделяет текущее операционное состояние и данные, связанные с историей изменений и audit trail. Это позволяет эффективно работать с текущими записями и сохранять трассируемость важных изменений.

Контролируемое публичное раскрытие данных

Публичные страницы, карточки растений, списки, фотографии и данные карты публикуются через отдельные public endpoints и DTO.

Visibility rules предотвращают случайное раскрытие внутренних данных тенанта.

Архитектурные компромиссы

1. Мягкая мультитенантность вместо полноценной

Контекст

Система проектируется для множества организаций (как мультитентная). На ранней стадии продукту важны низкая операционная сложность, высокая скорость разработки и достижения функциональных требований (таксономический слой и возможность развивать сетевые сценарии между организациями).

Принятое решение

Для изоляции данных используется логическая multi-tenancy модель через атрибут root_unit_id. Корневое подразделение организации является границей организации, все сущности внутри дерева получают ссылку на этот root unit.

Отклонённая альтернатива

database-per-tenant или schema-per-tenant.

Обоснование
  • снижение инфраструктурной сложности на ранней стадии
  • допускает поглощение организаций или выделение отделов в организации с минимальной миграцией сущностей
Компромиссы
  • Изоляция данных логическая, а не физическая, поэтому любая ошибка фильтрации тенанта в любом API может привести к нарушению изолированности тенантов.
  • Сложнее выполнять резервное копирование данных отдельной организации, их восстановление, экспорт и физическое удаление (право на забвение).
  • Сложнее выделить крупного клиента на отдельную инфраструктуру.
  • Шардинг и региональное разделение данных потребуют дополнительного проектирования.
Компенсирующие меры
  • Все сущности, принадлежащие определенной организации, несут root_unit_id.
  • Доступ ограничивается на нескольких уровнях: запросы репозиториев, спецификации JPA, проверки уровня бизнес-логики, авторизация на уровне методов контроллеров.
  • Центральная авторизация вынесена в AccessControlService.
  • Публичные API возвращают только public DTO и не раскрывают внутренние поля.
  • Для критичных сценариев написаны интеграционные тесты запрета доступа к чужому тенанту.
  • В целях соответствия 152-ФЗ выделена отдельная зона доступности для пользователей РФ (всего пока два). Запланировано проектирование механизма репликации публичных данных между зонами доступности с учетом возможного отключения РФ-сегмента сети от глобального интернета.
Триггер пересмотра
  • появление enterprise-клиентов с требованием физической изоляции
  • рост объёмов данных до уровня noisy-neighbor проблем
  • необходимость регионального хранения данных или юридическом требовании отделять данные организаций физически.

2. Модульный монолит вместо микросервисов

Контекст

Продукт с широкой доменной моделью: растения, таксономия, культивары, списки, места, импорт, публичные страницы, пользователи, роли, медиа и обмен между организациями. Команда маленькая, а доменная модель ещё активно уточняется. Кроме того не ясно, насколько будет разниться нагрузка на отдельные сущности.

Принятое решение

Backend реализован как модульный монолит на Spring Boot: единый артефакт, но с разделением по доменным зонам через контроллеры, сервисы, репозитории, DTO.

Отклонённая альтернатива

Набор микросервисов: сервис таксономии, сервис коллекций (+ импорт), сервис медиа, сервис авторизации и идентификации, сервис ГИС, сервис публичных ресурсов.

Почему это разумно сейчас

Монолит снижает оверхед от распределенных систем: нет сетевых контрактов между сервисами, нет распределенных транзакций, необходимости строить инфраструктуру поиска сервисов, нет сложной обозреваемости и оркестрации межсервисных отказов. Это ускоряет доставку и позволяет держать доменную модель целостной, пока продукт ещё не вышел за границы первых пилотов.

Компромиссы
  • Нельзя независимо масштабировать отдельные доменные модули.
  • Ошибка в одном модуле может повлиять на весь backend.
  • Со временем появляется риск неявных зависимостей между доменными областями.
  • Импорт, медиа и GIS могут иметь разные профили нагрузки, но пока живут в одном приложении.
Компенсирующие меры
  • Жёсткое пакетное разделение по доменным областям. Сервисный слой как граница для бизнес-логики, DTO как граница API. Тем самым снижается стоимость выделения ограниченного контекста в сосбтвенный сервис
  • Асинхронное выполнение тяжёлых импортов, чтобы не держать один поток и не хватать таймауты.
Триггер пересмотра

Выделение сервисов имеет смысл, когда конкретный модуль получает независимый масштаб нагрузки, отдельную команду владения, отдельный график релизов или отдельные требования к отказоустойчивости. Первые кандидаты на выделение: import pipeline, media processing/storage gateway, public map/search read model.


3. Монорепозиторий для backend и frontend вместо отдельных репозиторий

Контекст

Проект разрабатывается небольшой командой, где один разработчик отвечает за архитектуру, backend, frontend, deployment и интеграцию. Для таких условий важнее скорость согласованных изменений, чем организационная независимость команд.

Принятое решение

Backend и frontend хранятся в одном репозитории.

Отклонённая альтернатива

Отдельные репозитории для backend, frontend, инфраструктура и документация.

Почему это разумно сейчас

Монорепо позволяет делать атомарные изменения API и UI, проще держать архитектурный контекст целиком, быстрее проводить full-stack рефакторинг и LLM-генерацию кода. Результат более стабильный потому что модель видит связанную картину продукта.

Компромиссы
  • Границы ответственности могут размываться при росте команды.
  • Сложнее ограничивать доступ к отдельным частям кодовой базы.
  • Выше риск широких изменений без понимания радиуса влияния.
Компенсирующие меры
  • Раздельные frontend/backend папки и независимые команды сборки и запуска.
  • Интеграционные тесты взаимодействия frontend и backend.
Триггер пересмотра

Раздельные репо станут оправданными, если появятся независимые команды с разным релизным циклом, разные политики доступа к коду, необходимость публиковать части системы независимо.


4. Docker Compose вместо cloud native.

Контекст

На ранней стадии нужно быстро разворачивать систему на VPS, демонстрировать продукт, проводить пилоты и держать инфраструктурные расходы низкими.

Принятое решение

Backend, frontend, PostgreSQL/PostGIS и MinIO запускаются в одном Docker Compose контуре.

Отклонённая альтернатива

Полноценная cloud-native инфраструктура с отдельными мощностями под БД, объектное хранилище, с автоскейлингом контейнеров, мониторинг и развертку с множественными зонами доступности.

Почему это разумно сейчас

Docker Compose даёт быстрый быстрый холодный старт, воспроизводимое окружение, низкую стоимость и простую операционную модель. Для MVP, демо-стенда и раннего пилота это лучше, чем преждевременная сложность cloud native.

Компромиссы
  • Один VPS является единой точкой отказа.
  • БД, объектное храниище и сервисы конкурируют за ресурсы.
  • Масштабирование в основном вертикальное.
  • Нет полноценной high availability модели, нет учёта пиков нагрузки. Нельзя считать продуктовой архитектурой.
  • Резервное копирование, восстановление и мониторинг становятся критичными операционными задачами.
Компенсирующие меры
  • Сервисы остаются stateless.
  • Конфигурация должна быть env-based.
  • Данные БД и объектного хранилища живут в персистивных томах с регулярными резервным копированием.
  • Reverse proxy / TLS / rate limits выносятся в инфраструктурный слой.
  • Целевой путь миграции должен быть заранее описан: отдельные инстансы PostgreSQL/PostGIS, S3-совместимые хранилища, горизонтальное масштабирование контейнеров сервисов.
Триггер пересмотра

Переход нужен при появлении платящих клиентов, SLA-ожиданий, роста объёма фото/импортов, требований к высокой доступности, регулярных простоев VPS или необходимости регионального размещения данных.

... для портфолио опубликована только часть компромиссов.

См. также Architecture Decision Records.

Дорожная карта и демонстрация

Дорожная карта

Фаза Цель Инфраструктура Exit criteria
MVP / demo показать рабочую систему и основные сценарии VPS + Docker Compose demo flow, backup, базовая наблюдаемость
Pilot загрузить реальные данные и собрать обратную связь VPS + регулярные backup-процедуры, при росте медиа перейти на внешний S3 импорт реальной коллекции, список UX/data проблем
Первые покупатели обеспечить предсказуемую эксплуатацию отдельная БД, внешнее S3-совместимое объектное хранлище, мониторинг платящая организация, процедура восстановления, поддержка
Рост подготовить масштабирование и региональные контуры отдельная БД, внешнее S3-совместимое объектное хранлище, отдельные workers (импорт), оптимизация чтения, региональная стратегия рост числа тенантов, SLA ожидания, региональное распределение тенантов

Скриншоты и демо

Глобальная карта

UI_1

Глобальная карта

Управление растениями

UI_2

Управление растениями

Управление местами и границами

UI_3

Управление местами и границами

Импорт растений с AI-поддержкой

UI_4

Импорт растений с AI-поддержкой

Что демонстрирует проект

Этот проект демонстрирует мои способности:

  • работать на стыке системного анализа, backend дизайна, имплементации и деплоя.
  • довести идею до запуска системы с несколькими зонами доступности и дорожной картой масштабирования
  • стратегически планировать имплементацию SaaS малого-среднего масштаба
  • безопасно и контролируемо применять LLM-генерацию кода для имплементации фичей

Architecture Decision Records

См. Architecture Decision Records.