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

573 lines
19 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 | | опционально |
| region_id | bigint | FK → regions, NULL | привязка к региону |
| status | user_status | NOT NULL, default `active` | |
| created_at / updated_at | timestamptz | NOT NULL | |
**Уникальность:** `UNIQUE (messenger_type, messenger_id)`.
### 4.2. `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.3. `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.4. `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. Члены организации
### 7.1. `members`
| Поле | Тип | Ограничения | Описание |
|---|---|---|---|
| id | bigint | PK | |
| user_id | bigint | FK → users, NULL | связь с аккаунтом |
| region_id | bigint | FK → regions | |
| full_name | text | NOT NULL | |
| type | member_type | NOT NULL, default `member` | член / резидент |
| status | text | NOT NULL, default `active` | |
| joined_at | date | | |
| created_at / updated_at | timestamptz | NOT NULL | |
---
## 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 ||--o{ USERS : "привязка"
REGIONS ||--|| REGION_CARDS : has
REGIONS ||--o{ REGION_LINKS : has
REGIONS ||--o{ MEMBERS : has
REGIONS ||--o{ REGION_GOAL_VALUES : has
REGIONS ||--o{ TASKS : has
REGIONS ||--o{ ARTIFACTS : has
REGIONS ||--o{ ADVICES : has
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)` | аудит |
| members | `(region_id, type)` | численность |
**Материализованные представления** `mv_region_metrics`, `mv_district_metrics` обновляются Celery-задачей после каждой загрузки выгрузки и по расписанию.
---
## 19. Открытые вопросы
1. **Члены vs пользователи** — нужна ли отдельная сущность `members` или достаточно `users` со статусом? (влияет на учёт численности)
2. **История срезов** — хранить все срезы (`history`) или только `point0` + `current`?
3. **Резиденты** — отдельная таблица или `member_type`?
4. **Мультирегиональность пользователя** — может ли пользователь быть привязан к нескольким регионам?
5. **Хранение артефактов** — MinIO на том же диске (единый диск сервера).
---
## 20. Следующие документы
- Документ 2: контракты API (REST).
- Документ 3: макеты экранов (mobile-first).