docs: add technical design doc 1 - data model

This commit is contained in:
apuc committed 2026-09-26 19:26:02 +03:00
1 parent 9cbae40aa4
commit d15f320fb3
1 file changed
+572
+572
View File
@@ -0,0 +1,572 @@
# Технический проект. Документ 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).