Files
opora/docs/tech-design/01-data-model.md
T

577 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Технический проект. Документ 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).