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

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

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

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

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

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

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

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 обеспечивают трассируемость чувствительных действий.