From d15f320fb3ed029a74bf49f4f9be2720be556fcd Mon Sep 17 00:00:00 2001 From: Kavalar Date: Sat, 26 Sep 2026 19:26:02 +0300 Subject: [PATCH] docs: add technical design doc 1 - data model --- docs/tech-design/01-data-model.md | 572 ++++++++++++++++++++++++++++++ 1 file changed, 572 insertions(+) create mode 100644 docs/tech-design/01-data-model.md diff --git a/docs/tech-design/01-data-model.md b/docs/tech-design/01-data-model.md new file mode 100644 index 0000000..fc93461 --- /dev/null +++ b/docs/tech-design/01-data-model.md @@ -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).