# Технический проект. Документ 1: Модель данных **Проект:** АС «Платформа ОПОРА РОССИИ» **СУБД:** PostgreSQL 16 **Дата:** 26.09.2026 **Статус:** черновик к согласованию --- ## 1. Общие принципы - **Единое хранилище** для бота и сайта (п. 4.1.1 ТЗ). - Первичные ключи — `bigint` (identity) либо `uuid` для внешне адресуемых сущностей (токены, сессии). - Все таблицы содержат `created_at` / `updated_at` (`timestamptz`). - Мягкое удаление (`deleted_at`) — для сущностей, где нужна история (карточки, задачи, артефакты). - Перечисления — через `enum`-типы PostgreSQL или `text` + `CHECK` (выбор на этапе реализации). - Денормализованные агрегаты (прирост, рейтинг) — материализованные представления, пересчитываемые Celery-задачей. - JSONB — для гибких полей (фильтры аудитории, payload аудита). --- ## 2. Перечисления (enum) | Enum | Значения | |---|---| | `messenger_type` | `telegram`, `max` | | `user_status` | `active`, `blocked`, `pending` | | `role_code` | `admin`, `coordinator`, `region_head`, `member`, `deputy` | | `login_token_status` | `active`, `used`, `expired`, `revoked` | | `snapshot_kind` | `point0`, `current`, `history` | | `member_type` | `member`, `resident` | | `task_status` | `new`, `in_progress`, `review`, `done` | | `task_priority` | `low`, `medium`, `high` | | `request_type` | `join`, `access`, `event`, `support` | | `request_status` | `new`, `in_progress`, `closed` | | `broadcast_type` | `info`, `education`, `rating` | | `broadcast_status` | `draft`, `scheduled`, `sending`, `sent`, `failed` | | `recipient_status` | `pending`, `sent`, `failed` | | `actor_type` | `user`, `assistant`, `system` | | `link_type` | `community`, `chat` | --- ## 3. Справочники ### 3.1. `federal_districts` — федеральные округа (8) | Поле | Тип | Ограничения | Описание | |---|---|---|---| | id | bigint | PK | | | name | text | NOT NULL, UNIQUE | «ЮФО», «ЦФО»… | | code | text | UNIQUE | краткий код | | created_at | timestamptz | NOT NULL | | ### 3.2. `regions` — регионы (89) | Поле | Тип | Ограничения | Описание | |---|---|---|---| | id | bigint | PK | | | name | text | NOT NULL, UNIQUE | «Донецкая Народная Республика» | | federal_district_id | bigint | FK → federal_districts | округ | | status | text | NOT NULL, default `active` | | | created_at / updated_at | timestamptz | NOT NULL | | ### 3.3. `roles` — роли | Поле | Тип | Ограничения | |---|---|---| | id | bigint | PK | | code | role_code | NOT NULL, UNIQUE | | name | text | NOT NULL | --- ## 4. Пользователи и доступ ### 4.1. `users` — аккаунты (в т.ч. члены организации) | Поле | Тип | Ограничения | Описание | |---|---|---|---| | id | bigint | PK | | | full_name | text | | ФИО | | messenger_type | messenger_type | NOT NULL | | | messenger_id | text | NOT NULL | внешний id в мессенджере | | phone | text | | опционально | | status | user_status | NOT NULL, default `active` | статус аккаунта | | member_type | member_type | NULL | `member` / `resident`; NULL — не член | | joined_at | date | NULL | дата вступления | | created_at / updated_at | timestamptz | NOT NULL | | **Уникальность:** `UNIQUE (messenger_type, messenger_id)`. **Решение:** отдельная таблица `members` не используется — членство выражается через `member_type` и `status` пользователя (учёт численности — агрегат по `users`). ### 4.2. `user_regions` — привязка пользователя к регионам (мультирегиональность) | Поле | Тип | Ограничения | |---|---|---| | id | bigint | PK | | user_id | bigint | FK → users | | region_id | bigint | FK → regions | | is_primary | boolean | NOT NULL, default false | | created_at | timestamptz | NOT NULL | **Уникальность:** `UNIQUE (user_id, region_id)`. Один пользователь может быть привязан к нескольким регионам; `is_primary` — основной. ### 4.3. `user_roles` — роли пользователей (с областью действия) | Поле | Тип | Ограничения | |---|---|---| | id | bigint | PK | | user_id | bigint | FK → users | | role_id | bigint | FK → roles | | region_id | bigint | FK → regions, NULL (область: своё отделение) | | granted_by | bigint | FK → users, NULL | | granted_at | timestamptz | NOT NULL | **Уникальность:** `UNIQUE (user_id, role_id, region_id)`. ### 4.4. `login_tokens` — одноразовые ссылки входа | Поле | Тип | Ограничения | Описание | |---|---|---|---| | id | uuid | PK | | | token | text | NOT NULL, UNIQUE | случайный, достаточной длины | | user_id | bigint | FK → users, NULL | может быть выдан до регистрации | | region_id | bigint | FK → regions, NULL | | | issued_by | bigint | FK → users (координатор) | | | status | login_token_status | NOT NULL, default `active` | | | expires_at | timestamptz | NOT NULL | | | used_at | timestamptz | NULL | | | created_at | timestamptz | NOT NULL | | **Правило:** повторное использование → `status = used`, ответ «Ссылка уже была использована…» (п. 4.1.5 ТЗ). ### 4.5. `sessions` — сессии | Поле | Тип | Ограничения | |---|---|---| | id | uuid | PK | | user_id | bigint | FK → users | | token_hash | text | NOT NULL, UNIQUE | | messenger_type | messenger_type | NOT NULL | | created_at | timestamptz | NOT NULL | | expires_at | timestamptz | NOT NULL | | revoked_at | timestamptz | NULL | --- ## 5. Цели и метрики ### 5.1. `goals` — цели | Поле | Тип | Ограничения | Описание | |---|---|---|---| | id | bigint | PK | | | name | text | NOT NULL | «Рост базы членов» | | description | text | | | | target_value | numeric | | «+1096» | | unit | text | | «чел.» | | is_active | boolean | NOT NULL, default true | | | created_at / updated_at | timestamptz | NOT NULL | | ### 5.2. `snapshots` — срезы | Поле | Тип | Ограничения | Описание | |---|---|---|---| | id | bigint | PK | | | goal_id | bigint | FK → goals | | | snapshot_date | date | NOT NULL | дата среза | | kind | snapshot_kind | NOT NULL | `point0` / `current` / `history` | | source | text | | `upload` / `manual` | | created_at | timestamptz | NOT NULL | | **Правило:** для цели ровно один `point0`; `current` — последний по дате. ### 5.3. `region_goal_values` — значения по регионам | Поле | Тип | Ограничения | |---|---|---| | id | bigint | PK | | snapshot_id | bigint | FK → snapshots | | region_id | bigint | FK → regions | | value | numeric | NOT NULL | | created_at | timestamptz | NOT NULL | **Уникальность:** `UNIQUE (snapshot_id, region_id)`. ### 5.4. `data_uploads` — загрузки выгрузок | Поле | Тип | Ограничения | |---|---|---| | id | bigint | PK | | goal_id | bigint | FK → goals | | file_key | text | NOT NULL (MinIO) | | uploaded_by | bigint | FK → users | | status | text | `uploaded` / `processed` / `failed` | | processed_at | timestamptz | NULL | | created_at | timestamptz | NOT NULL | ### 5.5. Материализованное представление `mv_region_metrics` Расчёт (п. 4.3.1 ТЗ): - `point0_value` — значение среза `point0`; - `current_value` — значение последнего `current`; - `growth = current_value − point0_value`; - `has_dynamics = growth > 0`; - `freshness_days = now() − snapshot_date` (для правила «не старше 7 дней»). Поля: `region_id`, `goal_id`, `point0_value`, `current_value`, `growth`, `has_dynamics`, `snapshot_date`, `freshness_days`, `updated_at`. ### 5.6. `mv_district_metrics` Агрегат по округу: `district_id`, `goal_id`, `growth_sum`, `regions_with_dynamics`, `regions_total`. --- ## 6. Карточка региона ### 6.1. `region_cards` | Поле | Тип | Ограничения | |---|---|---| | region_id | bigint | PK, FK → regions | | population | integer | численность | | head_user_id | bigint | FK → users, NULL | | updated_at | timestamptz | NOT NULL | ### 6.2. `region_links` — сообщества и чаты | Поле | Тип | Ограничения | |---|---|---| | id | bigint | PK | | region_id | bigint | FK → regions | | type | link_type | NOT NULL | | title | text | | | url | text | NOT NULL | | created_at / updated_at | timestamptz | NOT NULL | ### 6.3. `region_card_history` — история изменений | Поле | Тип | Ограничения | |---|---|---| | id | bigint | PK | | region_id | bigint | FK → regions | | field | text | NOT NULL | | old_value | text | | | new_value | text | | | changed_by | bigint | FK → users | | changed_at | timestamptz | NOT NULL | --- ## 7. Члены организации **Решение:** отдельная таблица `members` не используется. Членство выражается через поля `users.member_type` (`member` / `resident`) и `users.status`, а привязка к регионам — через `user_regions` (раздел 4). Учёт численности — агрегат по `users` с фильтром по региону и типу. --- ## 8. Артефакты ### 8.1. `artifacts` | Поле | Тип | Ограничения | Описание | |---|---|---|---| | id | bigint | PK | | | title | text | | | | file_key | text | NOT NULL | ключ в MinIO | | file_name | text | NOT NULL | | | mime_type | text | | | | size_bytes | bigint | | до 50 МБ (п. 4.2.8.1) | | region_id | bigint | FK → regions, NULL | привязка | | goal_id | bigint | FK → goals, NULL | привязка | | task_id | bigint | FK → tasks, NULL | привязка | | uploaded_by | bigint | FK → users | | | deleted_at | timestamptz | NULL | мягкое удаление | | created_at | timestamptz | NOT NULL | | --- ## 9. Задачи (трекер) ### 9.1. `tasks` | Поле | Тип | Ограничения | |---|---|---| | id | bigint | PK | | title | text | NOT NULL | | description | text | | | due_date | date | | | priority | task_priority | NOT NULL, default `medium` | | status | task_status | NOT NULL, default `new` | | region_id | bigint | FK → regions, NULL | | assignee_user_id | bigint | FK → users, NULL | | assignee_role | role_code | NULL | | created_by | bigint | FK → users | | deleted_at | timestamptz | NULL | | created_at / updated_at | timestamptz | NOT NULL | ### 9.2. `task_comments` | Поле | Тип | Ограничения | |---|---|---| | id | bigint | PK | | task_id | bigint | FK → tasks | | author_id | bigint | FK → users | | body | text | NOT NULL | | created_at | timestamptz | NOT NULL | --- ## 10. База знаний ### 10.1. `knowledge_categories` | Поле | Тип | Ограничения | |---|---|---| | id | bigint | PK | | name | text | NOT NULL, UNIQUE | ### 10.2. `knowledge_articles` | Поле | Тип | Ограничения | |---|---|---| | id | bigint | PK | | title | text | NOT NULL | | body | text | NOT NULL | | category_id | bigint | FK → knowledge_categories, NULL | | created_by | bigint | FK → users | | created_at / updated_at | timestamptz | NOT NULL | --- ## 11. Календарь событий ### 11.1. `events` | Поле | Тип | Ограничения | |---|---|---| | id | bigint | PK | | title | text | NOT NULL | | description | text | | | starts_at | timestamptz | NOT NULL | | ends_at | timestamptz | | | location | text | | | created_by | bigint | FK → users | | created_at / updated_at | timestamptz | NOT NULL | ### 11.2. `event_reminders` | Поле | Тип | Ограничения | |---|---|---| | id | bigint | PK | | event_id | bigint | FK → events | | user_id | bigint | FK → users | | remind_at | timestamptz | NOT NULL | | sent_at | timestamptz | NULL | --- ## 12. Запросы (заявки) ### 12.1. `requests` | Поле | Тип | Ограничения | |---|---|---| | id | bigint | PK | | type | request_type | NOT NULL | | user_id | bigint | FK → users | | region_id | bigint | FK → regions, NULL | | subject | text | | | body | text | | | status | request_status | NOT NULL, default `new` | | assigned_to | bigint | FK → users, NULL | | created_at / updated_at | timestamptz | NOT NULL | ### 12.2. `request_comments` | Поле | Тип | Ограничения | |---|---|---| | id | bigint | PK | | request_id | bigint | FK → requests | | author_id | bigint | FK → users | | body | text | NOT NULL | | created_at | timestamptz | NOT NULL | --- ## 13. Советы от регионов ### 13.1. `advices` | Поле | Тип | Ограничения | |---|---|---| | id | bigint | PK | | region_id | bigint | FK → regions | | goal_id | bigint | FK → goals, NULL | | author_user_id | bigint | FK → users | | body | text | NOT NULL | | status | text | `published` / `hidden` | | created_at | timestamptz | NOT NULL | --- ## 14. Рассылки ### 14.1. `broadcast_templates` | Поле | Тип | Ограничения | |---|---|---| | id | bigint | PK | | code | text | NOT NULL, UNIQUE | | name | text | NOT NULL | | type | broadcast_type | NOT NULL | | body_template | text | NOT NULL (с плейсхолдерами) | | created_at / updated_at | timestamptz | NOT NULL | ### 14.2. `broadcasts` | Поле | Тип | Ограничения | |---|---|---| | id | bigint | PK | | template_id | bigint | FK → broadcast_templates, NULL | | type | broadcast_type | NOT NULL | | subject | text | | | body | text | | | audience_filter | jsonb | регион/округ/роль/подписка | | status | broadcast_status | NOT NULL, default `draft` | | scheduled_at | timestamptz | NULL | | started_at / finished_at | timestamptz | NULL | | created_by | bigint | FK → users | | created_at | timestamptz | NOT NULL | ### 14.3. `broadcast_recipients` | Поле | Тип | Ограничения | |---|---|---| | id | bigint | PK | | broadcast_id | bigint | FK → broadcasts | | user_id | bigint | FK → users | | status | recipient_status | NOT NULL, default `pending` | | sent_at | timestamptz | NULL | | error | text | NULL | **Уникальность:** `UNIQUE (broadcast_id, user_id)`. --- ## 15. История платформы ### 15.1. `platform_history` | Поле | Тип | Ограничения | |---|---|---| | id | bigint | PK | | title | text | NOT NULL | | body | text | | | event_date | date | | | is_public | boolean | NOT NULL, default true | | created_by | bigint | FK → users | | created_at | timestamptz | NOT NULL | --- ## 16. Аудит и ассистент ### 16.1. `audit_log` | Поле | Тип | Ограничения | |---|---|---| | id | bigint | PK | | actor_user_id | bigint | FK → users, NULL | | actor_type | actor_type | NOT NULL | | action | text | NOT NULL | | entity_type | text | | | entity_id | text | | | payload | jsonb | | | ip | inet | | | created_at | timestamptz | NOT NULL | ### 16.2. `assistant_configs` | Поле | Тип | Ограничения | |---|---|---| | id | bigint | PK | | owner_user_id | bigint | FK → users, UNIQUE | | enabled | boolean | NOT NULL, default false | | permissions | jsonb | разрешённые действия | | created_at / updated_at | timestamptz | NOT NULL | ### 16.3. `assistant_actions` | Поле | Тип | Ограничения | |---|---|---| | id | bigint | PK | | owner_user_id | bigint | FK → users | | action | text | NOT NULL | | entity_type | text | | | entity_id | text | | | payload | jsonb | | | status | text | `ok` / `failed` | | created_at | timestamptz | NOT NULL | --- ## 17. ERD (основные связи) ```mermaid erDiagram FEDERAL_DISTRICTS ||--o{ REGIONS : contains REGIONS ||--|| REGION_CARDS : has REGIONS ||--o{ REGION_LINKS : has REGIONS ||--o{ REGION_GOAL_VALUES : has REGIONS ||--o{ TASKS : has REGIONS ||--o{ ARTIFACTS : has REGIONS ||--o{ ADVICES : has USERS ||--o{ USER_REGIONS : "привязан к" REGIONS ||--o{ USER_REGIONS : includes USERS ||--o{ USER_ROLES : has ROLES ||--o{ USER_ROLES : grants USERS ||--o{ LOGIN_TOKENS : receives USERS ||--o{ SESSIONS : has USERS ||--|| ASSISTANT_CONFIGS : owns USERS ||--o{ ASSISTANT_ACTIONS : performs GOALS ||--o{ SNAPSHOTS : has SNAPSHOTS ||--o{ REGION_GOAL_VALUES : contains GOALS ||--o{ DATA_UPLOADS : receives TASKS ||--o{ TASK_COMMENTS : has TASKS ||--o{ ARTIFACTS : links KNOWLEDGE_CATEGORIES ||--o{ KNOWLEDGE_ARTICLES : groups EVENTS ||--o{ EVENT_REMINDERS : triggers REQUESTS ||--o{ REQUEST_COMMENTS : has BROADCAST_TEMPLATES ||--o{ BROADCASTS : based_on BROADCASTS ||--o{ BROADCAST_RECIPIENTS : sends ``` --- ## 18. Индексы и производительность | Таблица | Индекс | Назначение | |---|---|---| | users | `(messenger_type, messenger_id)` UNIQUE | поиск при входе | | login_tokens | `(token)` UNIQUE, `(status, expires_at)` | проверка ссылки | | region_goal_values | `(snapshot_id, region_id)` UNIQUE | целостность | | region_goal_values | `(region_id)` | выборки по региону | | tasks | `(region_id, status)`, `(assignee_user_id, status)` | фильтры трекера | | artifacts | `(region_id)`, `(task_id)`, `(goal_id)` | привязки | | broadcast_recipients | `(broadcast_id, status)` | прогресс рассылки | | audit_log | `(actor_user_id, created_at)`, `(entity_type, entity_id)` | аудит | | user_regions | `(region_id)`, `(user_id)` | численность, привязки | **Материализованные представления** `mv_region_metrics`, `mv_district_metrics` обновляются Celery-задачей после каждой загрузки выгрузки и по расписанию. --- ## 19. Принятые решения 1. **Члены vs пользователи** — отдельная таблица `members` не используется; членство через `users.member_type` + `users.status`. 2. **История срезов** — хранятся **все** срезы (`snapshot_kind = history`), не только `point0` + `current`. 3. **Резиденты** — через `member_type` (`member` / `resident`), без отдельной таблицы. 4. **Мультирегиональность** — пользователь может быть привязан к нескольким регионам через `user_regions`. 5. **Хранение артефактов** — MinIO на едином диске сервера (отдельного диска нет). --- ## 20. Следующие документы - Документ 2: контракты API (REST). - Документ 3: макеты экранов (mobile-first).